Realtime Security
The Volcano Realtime security model: authentication flow, channel authorization, and best practices for securing realtime features.
Volcano Realtime is designed with security-first principles. This document covers the security model, authentication flow, and best practices for securing your realtime features.
Authentication
Volcano Realtime supports multiple authentication modes:
Mode 1: User Token + Anon Key (Frontend)
For frontend applications with authenticated users.
const realtime = new VolcanoRealtime({
apiUrl: 'https://api.yourapp.com',
anonKey: 'your-anon-key', // Required: identifies project
accessToken: userToken, // Required: authenticates user
});Requirements:
- Anon key: Contains project ID (safe to expose client-side)
- Access token: User's JWT from Volcano Auth (contains user ID, project ID)
- Session must be valid (not expired, not revoked)
- User must not be banned
Mode 2: Service Role Key (Backend)
For backend services and server-to-server communication.
// Service key only - project ID inferred from key
const realtime = new VolcanoRealtime({
apiUrl: 'https://api.yourapp.com',
anonKey: '', // Not required
accessToken: 'sk-your-key', // Service role key
});
// With anon key for extra validation
const realtime = new VolcanoRealtime({
apiUrl: 'https://api.yourapp.com',
anonKey: 'your-anon-key', // Optional: must match service key project
accessToken: 'sk-your-key',
});Requirements:
- Service key must exist in database (not deleted)
- If anon key is provided, project IDs must match
- Service key JWT project must match database record
Security notes:
- A service key holding
*receives Postgres changes with RLS bypassed. Until Volcano applies service key scopes to realtime, so does every other service key - Once Volcano applies service key scopes to realtime, a key scoped without
*needsrealtime.subscribeto subscribe to broadcast and presence channels,realtime.publishto publish, and*for Postgres changes. An open subscription the scope doesn't cover ends within seconds, with unsubscribe code2100and reasonservice key scope. Durable run channels keep their rule - Use only in trusted backend environments
- Never expose in client-side code
Authentication Comparison
| Credential | User Token Mode | Service Key Mode |
|---|---|---|
| Anon Key | Required | Optional |
| Access Token | User JWT | Service Key |
| RLS Enforcement | Yes | Bypassed |
| Use Case | Frontend apps | Backend services |
| Session Check | Yes | No |
| Ban Check | Yes | No |
Automatic project isolation
You never need to specify a project ID. The project is automatically determined from your anon key:
// ✅ Correct - project derived from anonKey
const realtime = new VolcanoRealtime({
apiUrl: 'https://api.yourapp.com',
anonKey: anonKey,
accessToken: accessToken,
});
// Channel names are simple - no project prefix needed
const chat = realtime.channel('chat-room');
const presence = realtime.channel('lobby', { type: 'presence' });
const db = realtime.channel('public:messages', { type: 'postgres' });The server:
- Extracts project ID from the anon key JWT
- Automatically prefixes all channels with the project ID
- Enforces that users can only access their project's channels
Cross-project attack prevention
Cross-project access is impossible by design:
- Anon key contains the project ID
- Server prefixes all channels with that project ID
- Client cannot override or specify a different project
If a malicious client attempts any manipulation:
- Invalid anon key → connection rejected
- Forged project in channel name → server ignores it, uses anon key project
Security events are logged to server logs:
[realtime] Connection accepted: user=user_12345 project=proj_abc123 role=anonymousChannel authorization
Broadcast channels (default)
Any authenticated user in the project can join:
const channel = realtime.channel('announcements');Presence channels
Any authenticated user can track and be tracked:
const channel = realtime.channel('lobby', { type: 'presence' });A connection reads a presence channel's members only after subscribing to it. The SDKs subscribe first.
Postgres changes channels
Respect Row Level Security. Users only receive events for rows they can SELECT:
const channel = realtime.channel('public:messages', { type: 'postgres' });
// User will only receive events for rows they can access via RLSThe server:
- Receives a database change notification
- For each subscriber, executes an RLS check
- Only forwards the event if the user can SELECT that row
Durable execution channels
A durable execution's channel admits the same callers as its messages API. The durable function's visibility has to admit the connection first; then:
- A service role key holding
*can watch any execution in the project. A key scoped to a list without*needsfunctions.invokeand the execution'sclient_token, sent with the subscribe request. The start returns that token. - A signed-in user, anonymous sign-ins included, can watch the executions they started.
- Any other signed-in user needs the execution's
client_token, which the SDK sends with the subscribe request. - Until an execution is
running, only a service role key holding*can watch it.
An anon key alone does not open a realtime connection, so a visitor who is not signed in can sign in anonymously first or poll the messages list instead.
A refused subscribe looks the same as one for an execution that does not exist. The check runs when the subscription is made and about once a minute while it is open, so a subscription ends soon after its function is made private. Clients cannot publish on a durable channel.
Banned users
Users banned via the Auth Admin API are automatically blocked:
- On connection attempt, server checks if user is banned
- Banned users receive an authentication error
- If a user is banned while connected, their connection is terminated
Session revocation
When a user's session is revoked:
- Revocation reaches every realtime server
- User's active connections are terminated within seconds
- User must re-authenticate to reconnect
Rate limiting
Realtime enforces rate limits to prevent abuse:
| Action | Limit |
|---|---|
| Concurrent connections | 200 (HOBBY), 100,000 (SUPERAGENT) |
| Channels per connection | 100 (HOBBY), 500 (SUPERAGENT) |
| Publishes per second | 10 per connection |
| Message size | 256 KB (HOBBY), 1 MB (SUPERAGENT) |
When a rate limit is exceeded:
- The action is rejected
- An event is logged to server logs
- Client receives an error response
Plan limits
Limits vary by plan:
| Limit | HOBBY | SUPERAGENT |
|---|---|---|
| Concurrent connections | 200 | 100,000 |
| Messages per month | 100,000 | Unlimited |
| Max message size | 256 KB | 1 MB |
| Channels per connection | 100 | 500 |
See Plans and limits for limits across all resources.
Monitoring
All security events are logged to server logs for monitoring:
Events logged:
- ✅
auth_success- Successful authentication - ❌
auth_failure- Failed authentication attempt - 🚨
cross_project_attempt- Cross-project access blocked - 🚫
banned_user_attempt- Banned user attempted to connect - ⚠️
rate_limited- Rate limit exceeded - 📡
disconnect- Client disconnected
View events in your server logs or use external monitoring tools.
Input validation
All client inputs are validated before processing:
Channel names
- Maximum length: 255 characters
- Only alphanumeric, hyphens, underscores, colons
- SQL injection patterns are blocked
- Path traversal attempts are blocked
Message payloads
- Maximum size: Configurable by plan
- Validated JSON structure
- Binary data must be base64 encoded
Presence data
- Maximum size: 1 KB
- Validated JSON structure
Best practices
1. Never expose access tokens
// ❌ DON'T: Hardcode tokens
const realtime = new VolcanoRealtime({
accessToken: 'eyJhbGci...',
});
// ✅ DO: Get token from auth flow
const { accessToken } = await volcano.auth.getSession();
const realtime = new VolcanoRealtime({
anonKey,
accessToken,
});2. Use token refresh callback
const realtime = new VolcanoRealtime({
apiUrl: 'https://api.yourapp.com',
anonKey,
accessToken,
getToken: async () => {
// Called when token needs refresh
const session = await volcano.auth.refreshSession();
return session.accessToken;
},
});3. Use RLS for Postgres changes
Always enable Row Level Security on tables with realtime:
-- Enable RLS
ALTER TABLE messages ENABLE ROW LEVEL SECURITY;
-- Users can only see messages in their rooms
CREATE POLICY "room_messages" ON messages
FOR SELECT USING (
room_id IN (
SELECT room_id FROM room_members
WHERE user_id = auth.uid()
)
);4. Validate presence data
Don't trust presence data from other users for authorization:
channel.on('join', (info) => {
// ❌ DON'T: Trust user-provided data for permissions
if (info.isAdmin) { ... }
// ✅ DO: Use server-validated data only
const isAdmin = await checkAdminStatus(info.userId);
});5. Sanitize broadcast data
// ❌ DON'T: Broadcast raw user input
await channel.send({ event: 'message', text: userInput });
// ✅ DO: Sanitize and validate
const sanitizedText = sanitizeHtml(userInput);
if (sanitizedText.length > 0 && sanitizedText.length < 10000) {
await channel.send({ event: 'message', text: sanitizedText });
}Security configuration
Configure security settings in Realtime > Settings:
| Setting | Description | Default |
|---|---|---|
| Max channels per connection | Limit channel subscriptions | 100 (HOBBY) / 200 (SUPERAGENT) |
| Max message size | Maximum broadcast payload | 32 KB (HOBBY) / 256 KB (SUPERAGENT) |
Incident response
If you suspect a security issue:
- Check server logs for unusual patterns
- Revoke sessions for affected users
- Ban users if malicious activity is confirmed
- Rotate keys if anon key or service key may be compromised
- Contact support for assistance with serious incidents
Compliance
Volcano Realtime supports common compliance requirements:
- Encryption in transit: All WebSocket connections use TLS
- Authentication required: No anonymous connections
- Server logging: All security events are logged to server logs
- Session management: Sessions can be revoked instantly
- Data isolation: Projects are strictly isolated
- RLS enforcement: Postgres changes filtered by RLS