This version is in beta. Some features may change before release.

Gating access

Decide who can join a room. The two gates (identity + GroupPolicy), the public-only default, and worked examples for private, membership, tenant, and role-based rooms.

Gating access to rooms

A client subscribes to a room by joining a group at the SSE/WS handshake. Whether that join is allowed is decided by two gates working together:

  1. Identity resolution - the framework figures out who is connecting (user_id: Option<&str>).
  2. Group policy - your GroupPolicy decides whether this user may join this group.

Identity is the user's primary key rendered to a string, so gating is PK-type-agnostic - an i64, String, or uuid::Uuid PK all arrive as the same canonical string ("42", "550e8400-…").

A group the policy rejects fails the whole handshake with 403 Forbidden before any data flows. This is default-deny: a client can never subscribe to a room it has no claim to.

Warning

Group names are the security boundary. Anything in a public: group is readable by anyone (the default policy lets anyone join public:*). Only name a group public: when its data is genuinely public. Everything sensitive belongs in a group the policy gates.

The default policy: public:* only

With no configuration, RealtimePlugin uses PublicGroupsOnly. Its rule is exactly one line: allow only groups whose name starts with public:.

Code
rust
// The built-in default - you get this for free:
RealtimePlugin::new() // PublicGroupsOnly
  • public:lobby, public:posts → anyone may join, signed in or not.
  • chat:123, tenant:99, user:7denied for everyone, including authenticated users.

To grant access to private rooms, override the policy and decide from the authenticated identity.

Gate 1: who is connecting (user_id)

Every policy receives user_id: Option<&str> - the authenticated user's PK string, or None for anonymous. The string is the user's primary key rendered to its canonical form, so it works for any PK type (i64"42", uuid"550e8400-…"). By default the identity resolver always returns None, so without wiring identity your policy can never see a real user.

Two ways to populate it:

Warning

Anonymous-by-default footgun. If you write a policy that grants user:{id} rooms but forget with_auth_sessions() / identity_resolver(...), user_id is always None and every private room is silently denied. Wire identity first, then write the policy.

Gate 2: the policy - group_policy_fn

The ergonomic way to gate rooms is a closure: |user_id: Option<&str>, group: &str| -> bool. user_id is the user's PK string (so the same gate works for i64, String, and uuid PKs); return true to allow the join. (Under the hood this wraps your closure in FnGroupPolicy.)

Public rooms - anyone

Code
rust
RealtimePlugin::new()
.group_policy_fn(|_user_id, group| group.starts_with("public:"));

Per-user private room - user:{id}

Each user gets a room only they can join:

Code
rust
RealtimePlugin::new()
.with_auth_sessions()
.group_policy_fn(|user_id, group| {
if group.starts_with("public:") {
return true; // public rooms: anyone
}
match user_id {
Some(uid) => group == format!("user:{uid}"), // your own private room
None => false, // anonymous: public only
}
});

Membership / multi-tenant - check the DB or the session

For a tenant:{id} or a team:{id} room, gate on a membership check. The closure is synchronous, so resolve membership from data you can read synchronously (a cache, or a check that doesn't need .await):

Code
rust
RealtimePlugin::new()
.with_auth_sessions()
.group_policy_fn(|user_id, group| {
if group.starts_with("public:") {
return true;
}
match user_id {
Some(uid) => is_member(uid, group), // your membership lookup -> bool
None => false,
}
});

Role-based - staff-only channels

Code
rust
RealtimePlugin::new()
.with_auth_sessions()
.group_policy_fn(|user_id, group| {
if group.starts_with("public:") {
return true;
}
if group.starts_with("staff:") {
return matches!(user_id, Some(uid) if is_staff(uid));
}
false
});
Info
A clean pattern for sensitive data that avoids gating logic entirely: never put it in a joinable group. Target it with `Realtime::to_user(id.to_string())` instead - a user-targeted send reaches only that user's own connections, so it's effectively a private channel a visitor can never subscribe to. `to_user` takes the PK string (`user.id().to_string()`, `uuid.to_string()`, or a literal `"42"`).

Complex policies: a named GroupPolicy type

When a closure isn't enough (you want state, or shared helpers across methods), implement the GroupPolicy trait on your own type and pass it to group_policy:

Code
rust
use umbral_realtime::{GroupPolicy, RealtimePlugin};
 
struct AppPolicy {
// ... any state you need
}
 
impl GroupPolicy for AppPolicy {
fn can_join(&self, user_id: Option<&str>, group: &str) -> bool {
if group.starts_with("public:") {
return true;
}
match user_id {
Some(uid) => self.allows(uid, group),
None => false,
}
}
}
 
RealtimePlugin::new()
.with_auth_sessions()
.group_policy(AppPolicy { /* ... */ });

The default can_join (if you don't override it) is the same group.starts_with("public:") rule as PublicGroupsOnly.

How it all fits

StepWhereResult
Resolve identitywith_auth_sessions() / identity_resolver(...)user_id: Option<&str> (the PK string)
Check the joingroup_policy_fn(...) / group_policy(...)allow → stream; deny → 403
Per-group dispatchRealtime::to_group(...).send(...)reaches only joined connections

Because every send to a group only reaches connections the policy already admitted, the policy is the single chokepoint for "who can see this room" - including for model subscriptions and presence, which both ride the same group dispatch.

Info

Transport-level security is separate from the policy. GroupPolicy decides who may join a room. On top of that, the WebSocket handshake is guarded against cross-site WebSocket hijacking (CSWSH) - cross-origin WS upgrades are rejected in production by default, because CORS does not cover WebSockets. If you serve a separate frontend origin, allow it with allowed_origins([...]). See the WebSocket Origin guard.

realtimesecuritygatingauthpermissions