Skip to content

Add permissions for rooms#2274

Open
nezaj wants to merge 6 commits intomainfrom
rooms-perms
Open

Add permissions for rooms#2274
nezaj wants to merge 6 commits intomainfrom
rooms-perms

Conversation

@nezaj
Copy link
Contributor

@nezaj nezaj commented Feb 10, 2026

Adds permission checks for room joins, gated by room type. Rules are defined under a $rooms key in permissions, keyed by room type, with a single join action. The CEL context exposes data.id and auth.

CleanShot 2026-02-10 at 15 23 55@2x

Default behavior

  • If $rooms is not defined in rules, all joins succeed (backwards compatible)
  • If $rooms is defined, room types not listed fall back to $rooms.$default if present, otherwise allow
  • Admin sessions bypass room permission checks

Rule syntax

const rules = {
  $rooms: {
    chat: {
      allow: {
        join: 'auth.id != null',
      },
      bind: ['isMember', "data.id in auth.ref('$user.groups.id')"],
    },
    $default: {
      allow: {
        join: 'false',
      },
    },
  },
};

This pattern also let's us expand to other actions we may want to support like broadcast and setPresence

How it works

Client: The client now sends room-type alongside room-id in the join-room WebSocket message. The room type is stored in the Reactor's room state so it persists across reconnects.

Server: When handling a join-room message, the server:

  1. Looks up $rooms rules for the app
  2. If no $rooms key exists → allow (backwards compat)
  3. If $rooms exists but client didn't send room-type → deny (old clients must upgrade)
  4. Finds the matching CEL program using fallback order: specific room type → $default
  5. Evaluates the CEL expression with {data: {id: "..."}, auth: ...}
  6. On denial, sends join-room-error instead of join-room-ok

Files changed

File Change
client/packages/core/src/Reactor.js Thread roomType through joinRoom, _tryJoinRoom, reconnect, subscribePresence, subscribeTopic
client/packages/core/src/index.ts Pass roomType to reactor
client/packages/react-common/src/InstantReactRoom.ts Pass room.type in hooks
client/packages/core/src/rulesTypes.ts Add $rooms type to InstantRules
server/src/instant/db/cel.clj Add "join" to action->compiler
server/src/instant/model/rule.clj Add $rooms validation, get-room-program! with fallback logic
server/src/instant/reactive/session.clj Add assert-room-permission!, modify handle-join-room!
server/test/instant/model/rule_test.clj rule validation and program compilation
server/test/instant/reactive/session_test.clj permission enforcement
client/sandbox/react-nextjs/pages/play/room-perms.tsx E2E play page

@@ -2284,6 +2287,15 @@ export default class Reactor {
getPresence(roomType, roomId, opts = {}) {
const room = this._rooms[roomId];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if someone joins two rooms, different roomType, but same id?

I wonder if now that we are using roomType, we should create a kind of composite key: roomType+roomId

(.build)))

(def ^:private ^CelCompiler cel-join-compiler
(-> (CelCompilerFactory/standardCelCompilerBuilder)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need a custom one? this looks like cel-runtime-compiler. Maybe the missing thing is ruleParams, but perhaps that's fine? (Maybe at some point we'll want to support ruleParams)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea Claude originally wanted to group this with view-delete but I thought conceptually it might be nicer to separate it

:else errors)))
[]
rules))
(dissoc rules "$rooms")))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would not add a dissoc here. (same reasoning as in rule-validation-errors)

{:message (.getMessage cel-issue)})))))))
(recur (next paths))))))

(defn get-room-program!
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels like there's a lot of special-purpose code written specifically for $rooms here. Why is it necessary to split it out like this?

(ex/assert-permitted!
:has-room-join-permission?
["$rooms" nil "join"]
false))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: would pass the room-type conditional where, and remove the when-not

(rs/send-event! store app-id sess-id {:op :join-room-ok
:room-id room-id
:client-event-id client-event-id})
(catch Exception e
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this necessary? I would have expected this exception would have bubbled up to a top-level exception handler

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without the try/catch, the error would bubble to handle-instant-exception, which sends a generic {:op :error} message. On the client, the Reactor handles that in _handleReceiveError — but that code doesn't know anything about rooms. It wouldn't set room.error or notify presence subscribers.

So without it, here's what the user would see:

  1. No join-room-ok sent (permission denied)
  2. No join-room-error sent (bubbled to generic handler)
  3. Generic :error sent, but Reactor doesn't route it to room state
  4. Room stays stuck on isLoading: true forever

The top-level handler includes client-event-id and original-event so Reactor could match it back to the room join. But that would mean adding room-specific routing logic inside the generic error handler

Wdyt?

Copy link
Contributor

@stopachka stopachka left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some ideas!

@github-actions
Copy link
Contributor

View Vercel preview at instant-www-js-rooms-perms-jsv.vercel.app.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants