Room Access Rules
This feature is in beta. Core behavior is stable, but some APIs or configuration may change before general availability.
Rooms use explicit room-level access checks under rooms[namespace].access.
Configuration
import { defineConfig } from '@edgebase/shared';
export default defineConfig({
rooms: {
game: {
access: {
metadata: (auth, roomId) => auth !== null,
join: (auth, roomId) => auth?.custom?.plan === 'pro',
action: (auth, roomId, actionType) => {
if (!auth) return false;
if (actionType === 'BAN_PLAYER') return auth.role === 'admin';
return true;
},
},
handlers: {
lifecycle: {
onJoin(sender, room) {
room.sendMessage('joined', { userId: sender.userId });
},
},
actions: {
MOVE(payload, room, sender) {
room.setPlayerState(sender.userId, (state) => ({
...state,
position: payload,
}));
},
},
},
},
},
});
Release Mode
Rooms are fail-closed in release mode.
- If
publicis not enabled - and the matching
access.*function is missing - then
metadata,join, andactionare denied
Use public only as an explicit opt-in:
rooms: {
lobby: {
public: {
metadata: true,
join: true,
},
},
}
access vs handlers
access.metadata- decides whether room metadata can be fetched
access.join- decides whether a player can enter the room
access.action- decides whether a client action is allowed
access.signal- decides whether a signal can be sent
access.media.publish- decides whether a track can be published
access.media.control- decides whether mute/unmute/device/unpublish is allowed
access.media.subscribe- decides whether a member can watch another member's tracks
handlers.lifecycle.onJoin- runs after join is accepted
handlers.actions- handles accepted action payloads
handlers.timers- handles server timers
Signal Access
Control which signals can be sent inside a room:
rooms: {
game: {
access: {
signal: (auth, roomId, event, payload) => {
if (!auth) return false;
// Only allow known signal types
return ['offer', 'answer', 'ice-candidate', 'cursor-move'].includes(event);
},
},
},
}
| Parameter | Type | Description |
|---|---|---|
auth | AuthContext | null | The sender's identity |
roomId | string | The room instance ID |
event | string | Signal event name |
payload | unknown | Signal payload |
Media Access (Alpha)
Media access rules use a nested structure with three gates:
rooms: {
meeting: {
access: {
media: {
subscribe: (auth, roomId, payload) => auth !== null,
publish: (auth, roomId, kind, payload) => {
if (!auth) return false;
// Only allow screen-share for moderators
if (kind === 'screen') return auth.role === 'moderator';
return true;
},
control: (auth, roomId, operation, payload) => auth !== null,
},
},
},
}
| Gate | Parameters | Purpose |
|---|---|---|
subscribe | (auth, roomId, payload) | Watch other members' tracks |
publish | (auth, roomId, kind, payload) | Publish audio/video/screen |
control | (auth, roomId, operation, payload) | Mute, unmute, device change, unpublish |
kind values: 'audio', 'video', 'screen'. operation values: 'mute', 'unmute', 'device', 'unpublish'.
In release mode, missing signal and media access rules deny by default, matching the same fail-closed behavior as metadata, join, and action.
Using auth.meta
Room access receives the same enriched auth context as HTTP routes.
export default defineConfig({
auth: {
handlers: {
hooks: {
enrich: async (auth) => ({
workspaceId: await lookupWorkspace(auth.id),
}),
},
},
},
rooms: {
board: {
access: {
join: (auth) => Boolean(auth?.meta?.workspaceId),
},
},
},
});
Room Sender
Lifecycle and action handlers receive a sender object with:
userIdconnectionIdrole
Use auth inside access. Use sender inside handlers after the join has been accepted.