Short answer: treat a kick as server-side authorization revocation, not as a message asking the browser to leave. For a developer-tool support room where a guest can share a screen, issue narrowly scoped admission tokens, record a monotonically increasing membership epoch on the server, close every connection bound to the revoked membership, and reject any reconnect carrying the old epoch. The client may render the notice; it must never decide whether the participant is still allowed in.
That is the choice I would want documented before the pager fires: the room authority owns admission, while WebRTC owns media transport. A polite leave event is useful UX, but it is not a moderation control.
How should a video room API kick a disruptive participant?
Consider a bounded tabletop incident rather than an invented production anecdote. A contractor joins a screen-share session to help debug a build. They become disruptive, so a moderator presses Kick. The signaling service sends a removal notification, and the visible tile disappears. Thirty seconds later, the same browser reconnects with its still-valid room credential and resumes publishing. The dashboard briefly looked healthy because the peer count fell. The page should have fired on the successful readmission.
The first action worked exactly as designed: one transport ended. The control failed because transport state and authorization state had been collapsed into one boolean called connected. WebRTC specifies the peer connection and its media behavior; an application still needs a signaling and authorization policy around room membership. Closing a peer connection does not, by itself, invalidate whatever credential the application accepts on the next signaling connection.
One green counter lied.
This distinction is the invariant: a participant is removed only when the server can no longer authorize that membership. Everything else is cleanup.
For a support room, the blast radius must be narrower than the user's whole account. Revoking an account-wide token because one person misbehaved in one session can throw them out of unrelated build reviews, while revoking only a socket permits another socket to return. Scope the authority to a subject, room, membership, role, allowed actions, and short validity window. The exact encoding matters less than server-side verification at every admission boundary.
Why can't the browser own the kick?
Because the browser is the least trustworthy witness in the room. It can ignore a notification, suppress a callback, preserve an old credential, open another tab, or reconnect after losing network. None of those behaviors requires a sophisticated attacker. Normal retry logic is enough.
Retries are hostile here.
A credible kick therefore has two paths. The control path atomically changes authoritative membership state and creates an auditable moderation result. The delivery path tells connected clients what happened and closes transports associated with that membership. If delivery is late or never arrives, admission still fails.
The distinction also changes what page fires. kick_notice_sent is not the success condition. A useful alert asks whether a revoked membership later passed admission, published a track, or started a screen share. That is the contradiction operators need to see. Dashboards full of green delivery counters can hide it.
Use idempotency as part of the command contract. Moderators double-click, requests time out after the server commits, and automation retries. Repeating the same command must return the same outcome rather than incrementing the epoch twice or creating contradictory audit records. Keep the command identifier distinct from the membership identifier; they answer different questions during a postmortem.
Put the epoch in the admission decision
The preventative code path can stay small. The following Go example omits token encoding and networking on purpose; it shows the decision that must be server-owned even when the surrounding signaling service is written in Node.js. The signed claims are untrusted input until signature, issuer, audience, and expiry checks have already succeeded.
package moderation
import (
"context"
"errors"
"time"
)
var ErrAdmissionDenied = errors.New("room admission denied")
type Claims struct {
SubjectID string
RoomID string
MembershipID string
Role string
Epoch uint64
ExpiresAt time.Time
CanPublishScreen bool
}
type Membership struct {
ID string
SubjectID string
RoomID string
Epoch uint64
RevokedAt *time.Time
}
type Store interface {
Membership(ctx context.Context, roomID, membershipID string) (Membership, error)
}
func AuthorizeScreenShare(ctx context.Context, store Store, c Claims, now time.Time) error {
if c.SubjectID == "" || c.RoomID == "" || c.MembershipID == "" {
return ErrAdmissionDenied
}
if !now.Before(c.ExpiresAt) || !c.CanPublishScreen {
return ErrAdmissionDenied
}
m, err := store.Membership(ctx, c.RoomID, c.MembershipID)
if err != nil {
return ErrAdmissionDenied
}
if m.RevokedAt != nil || m.Epoch != c.Epoch || m.SubjectID != c.SubjectID {
return ErrAdmissionDenied
}
return nil
}
Fail closed here. If the membership store is unavailable, admitting a cached credential preserves availability by spending the moderation guarantee, which is the wrong trade for a room displaying source code, terminals, or credentials. Existing media may have its own bounded failure policy, but new admission and new screen publication should not silently bypass the authority check.
The kick transaction should lock or compare-and-swap the membership, mark it revoked, advance its epoch, and append the audit event before fan-out begins. A connection registry keyed by membership ID then identifies every current transport to close. Do not key only by user ID: one user may legitimately occupy several rooms, and a room-scoped moderation action should not leak across them.
Race tests matter more than a happy-path UI test. Run admission and revocation concurrently behind a barrier, then assert that no publication becomes authorized after the revocation commit point. Repeat with two tabs, a stale credential, a delayed notification, a disconnected moderator, and a retry using the same command ID. Three states are worth logging together: presented epoch, current epoch, and the final admission decision. Avoid putting the raw token in logs.
Choosing the enforcement boundary
There are several defensible designs, but they protect different boundaries. A client-only leave message is fast to build and appropriate for voluntary departure; it cannot enforce a kick. Closing a server-managed peer connection removes current media promptly, yet permits reentry if admission credentials remain valid. Short-lived tokens reduce the window, but waiting for expiry leaves a known-bad membership usable during that window. An authoritative revocation check on admission and publication costs a state lookup, while giving the control a precise commit point and an observable invariant.
That lookup is a deliberate latency and availability trade-off. Cache it only if the cache's invalidation path is part of the moderation transaction and its maximum staleness fits the room's threat model. A five-minute cache is not a minor implementation detail when the promised outcome is immediate removal. Measure revocation-to-disconnect time separately from revocation-to-denied-reconnect time; combining them produces a comfortable percentile that answers neither operational question. For the incident exercise, I would reject a design review that offered only one blended latency graph, because the graph could improve while stale credentials continued to pass admission; the two measurements represent different controls, have different failure owners, and demand different pages.
Role changes deserve the same model. If a guest loses screen-share permission but may remain in the call, advance the authorization version, stop the affected publication, and require subsequent publish attempts to present the current scope. A full membership revocation is too broad for that job. This is why one undifferentiated room_admin claim ages badly: it cannot express what changed without granting more authority than the client needs.
Where does this approach not fit?
This approach has real limitations. It adds an authoritative state dependency to admission, forces careful cache invalidation, and can deny new joins during a store outage. For a local demo, a trusted internal pairing tool, or a room with no identity and no moderation promise, closing the current connection may be an honest boundary. Say so in the product behavior. Do not label it a kick if refresh restores access.
At the other extreme, high-risk rooms may need more than an epoch: device binding, independent policy evaluation, retention rules for audit evidence, and immediate rotation of capabilities exposed during screen share. Those controls are organizational decisions, not features supplied by WebRTC. The transport standard is necessary context, but it cannot choose who belongs in the room.
The operational acceptance test is blunt: after the moderation transaction commits, can the same membership reconnect or publish from any existing client? If yes, the participant was disconnected, not removed. Page on that invariant, not on the animation that made a tile disappear.












