Skip to main content

Room Access Rules

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 public is not enabled
  • and the matching access.* function is missing
  • then metadata, join, and action are 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
  • handlers.lifecycle.onJoin
    • runs after join is accepted
  • handlers.actions
    • handles accepted action payloads
  • handlers.timers
    • handles server timers

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:

  • userId
  • connectionId
  • role

Use auth inside access. Use sender inside handlers after the join has been accepted.

See Also