Capabilities are the permissions the server granted this participant
in this call. Different across rooms, roles, and token scopes — and
they’re the single source of truth for what your UI should let the
user do. The SDK surfaces them as call.self.capabilities, a
SelfCapabilities instance with both synchronous getters and
observable streams.
Use capability flags to decide which UI affordances to render. Don’t guess from token type or hard-code “agents can lock rooms” — the server already knows, and the capability stream reflects what it decided for this specific call.
Once a call reaches joined, the server sends a call.joined event
carrying the participant’s capability flags. The SDK decodes them
into a structured SelfCapabilities object on call.self:
SelfCapabilities exposes both synchronous getters (end,
screenshare, setLayout, …) and $-suffixed observables that
re-emit when capabilities change. State updates are full
replacements — a new call.joined swaps the entire state object,
not a partial merge.
Capabilities only re-emit when the server sends a fresh call.joined
event. If your application supports role promotions (guest → host,
attendee → moderator) and you need the UI to react, the server has
to re-emit call.joined for that participant. The SDK supports
nested call.joined events and will update the capability state
when one arrives — but it won’t synthesize updates on its own.
SelfCapabilitiesSelfCapabilities groups flags into two families. Each capability
is exposed in two forms — an observable (e.g. end$) for reactive
bindings and a synchronous getter (e.g. end) for snapshot reads.
Member-level: what can be done to a member. The same eleven
fields (MemberCapabilities) apply twice:
On self, flags like remove and position read as self-actions
(can I leave the call, can I move my own tile); on member the
same flags read as moderation (can I kick others, can I move their
tile).
Call-level: end$, setLayout$, sendDigit$,
screenshare$, device$, plus on/off-split lock$ and
vmutedHide$.
The eleven member-level fields:
Each member flag is either a boolean (the action is allowed or not) or
an OnOffCapability — which separates “can turn this on” from “can
turn this off” because some roles can do one but not both (e.g. a
moderator who can lock a room while only the host can unlock it).
The pattern: subscribe once to the observable you care about, toggle the affordance, let the stream update it forever.
If you’re using the web components,
<sw-call-controls> already does this internally — buttons hide
themselves when the corresponding capability isn’t granted. You only
need the manual wiring when building a custom UI.
state$ emits the entire CallCapabilitiesState on every change.
Useful if you serialize the capability set into your own store:
It’s tempting to skip the capability stream and say “guests can’t end calls” in the UI. Two reasons not to:
call.joined after a permission change, the capability
stream reflects the new state. Token-based gating would be stuck
on the value the token was minted with.The local capability stream and the server are always in sync because
they come from the same call.joined event. Trust it.
Capabilities you see locally are exactly what the platform enforces
— calling participant.remove() without the member.remove
capability will fail server-side. The local checks are UX, not
security: the server is the authority, the flags exist so your UI
doesn’t show buttons that would error out.
Per-user presence isn’t currently exposed through the SDK. Derive online state from your own application telemetry — a last-seen heartbeat from your backend, or “currently in a call” tracked from your own call lifecycle webhooks.
SelfCapabilities — the classself$ / self — self capabilitiesmember$ / member — other-member capabilitiesend$, setLayout$, sendDigit$, screenshare$, device$, lock$, vmutedHide$ — call-level capabilitiesMemberCapabilities, OnOffCapability, CallCapabilitiesState — the data shapes