Build · Client integration
Authorization and revocation
Give each client the operations it needs. Apply visibility separately, and understand where revocation takes effect.
A product user authenticates with your application. An RMC participant authenticates with a room token. Your backend connects the two: create or locate a room, decide what that user may do, and return a participant credential with those grants.
For the identity model behind these checks, start with participants. The architecture walkthrough shows where current authority is applied.
Bootstrap on your backend
const created = await client.createRoom("support", serverAdminToken);
const owner = client.room(created.room.id, created.token);
await owner.createChannel({id: "chat", type: "text", direction: "bidirectional"});
After authenticating the application user, issue the required grants:
const browser = await owner.invite("customer", [
{action: "event:publish", resource: "chat"},
{action: "event:subscribe", resource: "chat"},
]);
const session = {
roomId: created.room.id,
participantId: browser.participant.id,
token: browser.token,
};
Return only session through the authenticated application endpoint. Here, client is an RmcClient and serverAdminToken is a backend secret. The invitation API accepts the grants provided by an authorized inviter; it is not a general user-policy engine. Treat participant:invite as a privileged operation. The product decides which grants to issue.
Keep the admin token and owner credential on the server. RMC room tokens are bearer credentials, so possession is authority. Do not put them in URLs, event payloads, example logs, or public build-time configuration. The quickstart demonstrates trusted bootstrap and separate participant credentials.
Grants answer “may this participant do this?”
A grant matches an action and a resource. Both accept exact strings, *, or a trailing prefix wildcard. A role name is not an implicit collection of grants.
| Operation | Action | Resource |
|---|---|---|
| Publish a channel event | event:publish |
Channel ID, such as chat |
| Read a channel event in history or subscription | event:subscribe |
Channel ID |
| Publish/read a channel-less event | event:publish / event:subscribe |
Event type, such as state.patched |
| Include a channel definition in a snapshot | channel:read or event:subscribe |
Channel ID |
| Read / write a state document | state:read / state:write |
Document key |
| Read / write an artifact | artifact:read / artifact:write |
Artifact type, not artifact ID |
| Read / start a delegation | delegation:read / delegation:start |
Task key |
| Cancel a delegation | delegation:cancel |
Delegation ID |
| Read conversation turns | conversation:read |
Turn ID |
| Invite / revoke a participant | participant:invite / participant:revoke |
* for invite; target participant ID for revoke |
| Close the room | room:close |
Room ID |
Read permission for a snapshot projection does not automatically grant access to its corresponding events. For example, a state view that hydrates preferences and consumes state.patched needs both state:read on preferences and event:subscribe on state.patched, subject to visibility. Event authorization for that channel-less type is type-wide, not a per-document-key selector.
The exact checks live in auth.go, session.go, and the HTTP handlers. Media has additional scoped grant rules.
Visibility answers “who is this fact for?”
An event must be visible and allowed by the participant's subscribe grant. A selector is an additional filter after these checks. It cannot reveal otherwise inaccessible records.
await room.channel("chat").publish("support.note.created", {
text: "A note for the assigned specialist.",
}, {
visibility: {participant_ids: [specialistParticipantId]},
});
specialistParticipantId must be an RMC participant ID for this room. The specialist still needs event:subscribe on chat.
| Visibility value | Meaning |
|---|---|
| Omitted / empty | Room-visible, subject to action grants |
{audience: "room"} |
Room-visible; this branch takes precedence over recipient lists |
{participant_ids: [id]} |
Visible to a listed participant |
{roles: ["specialist"]} |
Visible to participants with a listed role |
| Participant IDs and roles together | Either list may match |
{audience: "internal"} |
Requires internal:read on *, as well as the operation's grant |
For restricted recipients, omit audience: "room". Adding recipient lists to a room-visible event does not narrow that visibility. This behavior is explicit in VisibleTo.
Revocation is enforced during use
The participant session loads current authority for publication, history, snapshots, and subscriptions. A publication queued before revocation rechecks authority when it reaches the room command. An existing subscription cannot keep using the grants from its initial handshake indefinitely; revocation also wakes idle readers.
// Trusted owner/backend: the SDK has no revoke helper at this revision.
const response = await fetch(
`${baseUrl}/v1/rooms/${encodeURIComponent(roomId)}/participants/${encodeURIComponent(participantId)}`,
{method: "DELETE", headers: {Authorization: `Bearer ${ownerToken}`}},
);
if (!response.ok) throw new Error(`Revoke failed: HTTP ${response.status}`);
Revocation prevents further authorized use; it cannot recall a payload already delivered to a browser or cancel arbitrary application code already running in a callback. Media cleanup is a separate provider operation. RMC commits local revocation and durable cleanup intent before trying provider removal, and retries failures. A 502 can therefore mean revoked locally, provider cleanup still pending. See the cleanup boundary.
Read the revocation subscription tests, multiplex tests, and media lifecycle tests for the enforced cases.
Keep privileged APIs at the host boundary
Embedded Go code can open an authenticated RoomSession with Runtime.OpenSession. Trusted host code can use SessionForParticipant with a known participant ID. Do not derive that ID directly from an untrusted request and treat it as authentication.
Runtime and Store are privileged host interfaces. Handing them to an untrusted client or integration bypasses the purpose of the participant boundary. Room agents have their own participant identity and their returned publications are re-authorized; use that path for event-driven embedded work.