Skip to content

Administration

Identity

The product is the OAuth 2.0 authorization server for its own clients: the web client and the iOS and Android apps. Tenants bring their own identity providers; the product brokers sign-in to them and issues its own tokens. Every endpoint below serves the tenant of the request's host, and an account belongs to one tenant: the same person has a separate account on each tenant.

Accounts

app_user.kindCreated bySigns in withEnds
anonymousthe first visit of a client without a sessionits session only7 days after its last session expired
guestaccepting a world invite sent by email, or opening a scheduled meeting's linkits session onlyat the end of the guest period, or when the meeting's link expires
registeredthe first sign-in with an identity providerany identity linked to itwhen the user or a tenant admin deletes it

First visit

A client without a session calls POST /api/v1/auth/anonymous with a display name, and the avatar the visitor chose (Avatars). The server creates an anonymous account and a session, and answers like the token endpoint. The tenant's anonymous flag (Tenancy) can refuse anonymous visitors with 403, and rooms whose policy is not public refuse anonymous accounts in any case.

Roles

app_user.role is member for every account, or admin for the tenant's admins, who reach the administration of Tenancy. Only a registered account acts as an admin. vws tenant admin grants the role, and so does an operator's invite (Central admin): the code exchange of an account that signs in with an address the tenant's admin_invite holds, unexpired, makes it an admin, uses the invite up and writes account.admin_invite_taken to the tenant's audit log. GET /api/v1/me answers the account's role, admin only for a registered admin, so that the client links admins to the administration.

Signing in upgrades the account

A client with an anonymous session sends a current access token of it, refreshed first when needed, along with the code exchange that completes a sign-in:

  • A new identity. The anonymous account becomes a registered one in place: same user id, name, avatar and player variables, now with the identity linked. Its earlier session stays until it expires, so connections opened with it stay up.
  • The identity of an existing account. The anonymous account merges into that account. Player variables the registered account lacks move over, and the registered account's values win a conflict. Proximity chat messages are attributed to the registered account. The anonymous account and its sessions end.
  • A guest who signs in gets the registered account, and the guest account ends.

Identities match on provider and subject, never on email, so a provider cannot take over an account by asserting someone else's address.

Provider names

Every sign-in stores the name the provider gives, as a display name, on the identity: identity.display_name, empty where the provider gives none, as emailed links do, or none that makes a display name. A room whose entry.providerName is forced (Tenancy) shows a player whose account has such a name under the name of its latest sign-in, in the room, its invitations and its spaces, whatever the account's own name; an account without one goes by its own. Other rooms show the account's own name, which a new account takes from the visitor's typed name, sent with the sign-in, or else from the provider.

Guests

Both kinds of guest accept an invite through POST /api/v1/invites/{token}/accept, which creates a guest account and a session.

  • World guests. A member of a world, or an admin of the tenant, invites someone by email with POST /api/v1/worlds/{world}/invites, naming the room of the world where the guest lands. Accepting creates a membership of role guest. Access lasts the tenant's guestLifetime, 7 days by default, and the inviter may pick any period from an hour to 30 days. A tenant sends as many world invites a month as its plan allows, 5 on the plan standard, so guests cannot stand in for paid accounts, or more as extra charges where its admins allow them (Tenancy). World invites need the guests flag (Tenancy), at the tenant or the world level.
  • The link. An invite is a random token, stored as its SHA-256, that works once until the period ends. The email carries https://<host>/invites/<token>, and the answer to the inviter carries it too, so that it can be shared without email. The page shows the world with GET /api/v1/invites/{token}, asks for a display name and accepts; the guest's session never outlasts the period, and the client continues to the room.
  • Meeting guests. Any member can schedule a meeting: a meeting room area, a start, an end and a title. A meeting may not overlap another in the same room, and the room stays open to everyone unless its participants lock it. The meeting's link, shared directly or by email, is an invite that works from 1 hour before the start until 2 hours after the scheduled end. Its guests land in the meeting room and cannot walk out of it; leaving ends the visit, and a locked meeting still admits them. Meeting links do not count against the invite limit.
  • Scheduling. POST /api/v1/rooms/{room}/meetings schedules a meeting in a meeting area of the room's layout for a member of the room's world, not a guest, or an admin of the tenant, where the guests flag allows it (Tenancy); a meeting lasts at most 24 hours and ends in the future. The answer carries the link, which the server keeps only as a hash, so that only the organiser sees it, and sends it to up to 20 addresses. A meeting that overlaps another of the room is refused with 409, a database constraint keeping meetings of a room apart. GET /api/v1/rooms/{room}/meetings lists the room's meetings that have not ended, for its world's members, and DELETE /api/v1/meetings/{meeting} cancels one for its organiser or an admin: its link stops working and its guests no longer enter. Every link brings any number of guests, each with an account of their own.
  • Meetings in outside tools. A meeting may carry outsideLink, the https address of a meeting in an outside tool such as Zoom, Microsoft Teams or Google Meet, up to 2000 characters; any other value is refused with 422. The meeting then takes place there: the invite email and the invite page give the link, and the meeting's area opens it instead of the platform's meeting from 15 minutes before the start until the end (Map format).
  • A meeting's guest belongs to the meeting: it enters the meeting's room only, while the meeting stands, and any other room refuses it with the code meeting_only. In the room it enters inside the meeting area, whose lock it ignores, and the room keeps it there. The account ends with the link, 2 hours after the meeting.
  • The end. When the period ends or the link expires, the server revokes the guest's sessions, closes their connections and deletes the account with its personal data, within the 10 minutes of the account sweep (Operations). Their chat messages stay, shown as a former guest's.

Identity providers

  • An identity_provider row describes an OpenID Connect provider: issuer, client id, client secret, scopes, allowed email domains, and how claims map to the display name and to tags. A row with a tenant belongs to that tenant; a row without one is a platform provider, such as Google or Microsoft accounts, that tenants can offer.
  • The tenant's identityProviders setting lists the providers it offers, in order; without it the tenant offers its own providers, then the platform's, then email. The product stores no passwords.
  • The provider email signs people in with a link sent to their address, for those without an account at an offered provider. The link works once, for 15 minutes, and completes only the sign-in that requested it; the address is the identity's subject.
  • Client secrets are sealed with AES-256-GCM under VWS_ENCRYPTION_KEY and never stored in the clear; without the key, a platform refuses to add providers and says why.
  • A tenant's admins manage its own providers under /api/v1/admin/identity-providers; the platform's are the operators' to manage. GET lists the tenant's providers, whatever their status, and every provider the tenant may offer (offerable): its own active ones, then the platform's, whose slugs its own shadow, then email, each with where it comes from. POST adds a provider with its slug, name, issuer, client id and client secret, and optionally its scopes (openid email profile by default, openid required), allowed email domains, name claim (name by default), tags claim, logo and status (active by default); PUT .../{provider} changes one, keeping the stored secret when the body has none; DELETE removes one, and accounts that signed in with it keep their other ways in. Adding or changing a provider fetches the issuer's discovery document and refuses an issuer that does not answer with one of its own. Outside development, every request to a tenant's issuer, for its discovery document, the code exchange and its keys, goes through no proxy and connects to no private, loopback, link-local or shared address, whatever the host name resolves to, as webhook deliveries do (Partner API); an issuer inside the platform's network is refused like one that does not answer, and a sign-in whose token endpoint lies there fails. Issuers and logos are https addresses, and in development also http ones. Slugs are lowercase letters, digits and dashes, unique in the tenant, and email and development are reserved. No answer contains the client secret, sealed or not, and every change goes to the audit log as identity_provider.created, identity_provider.updated or identity_provider.deleted, without the secret. The administration's Sign-in page shows them and the callback address to register at a provider, https://<host>/api/v1/oauth/callback for each of the tenant's hosts.
  • The platform provider of VWS_OIDC_* signs operators into the central admin (Central admin), where they also manage the platform's providers for every tenant.
  • In development, a built-in provider development signs in any email address and name through a form at /api/v1/oauth/development, so local work and the end-to-end suite need no external service. Outside development it is neither offered nor served, and the form stays out of api/openapi.yaml.
  • The provider email is offered when VWS_EMAIL_BACKEND is not off. In development the log backend writes the link to the server's log instead of sending it.

Clients

Clientauth_client.kindRedirect URIRefresh token
webwebhttps://<host>/auth/callback for every verified host of the tenantan httpOnly cookie
ios, androidios, androidhttps://<host>/app/callback, claimed by the app as a universal link or app linkthe token response
  • Web redirect URIs come from the tenant's verified domains, so a new domain needs no client change.
  • Apps published under a tenant's brand get clients of that tenant (auth_client.tenant_id set) with their own redirect URIs. The tenant's hosts serve /.well-known/apple-app-site-association and /.well-known/assetlinks.json for them.
  • Apps sign in through the system browser with AppAuth (RFC 8252), never in an embedded web view.
  • Redirect URIs match exactly, path included.

Endpoints

EndpointPurpose
GET /.well-known/oauth-authorization-serverRFC 8414 metadata: the issuer, the endpoints, code responses, the authorization_code and refresh_token grants, S256, public clients
GET /.well-known/jwks.jsonthe public keys that verify access tokens
GET /api/v1/auth/providersthe identity providers the tenant offers: slug, name, logo
GET /api/v1/oauth/authorizestarts a sign-in
GET /api/v1/oauth/callbackthe identity provider's return
POST /api/v1/oauth/tokenexchanges a code, or refreshes
POST /api/v1/oauth/revokeends the session of a refresh token (RFC 7009)
POST /api/v1/auth/anonymouscreates an anonymous account and its session
POST /api/v1/auth/emailsends a sign-in link for a pending sign-in with the provider email
GET /api/v1/invites/{token}what an invite leads to, while it can be accepted
POST /api/v1/invites/{token}/acceptcreates a guest account and its session, for a world invite or a meeting link
GET /api/v1/methe account of the access token
GET /api/v1/me/sessionsthe account's sessions, the current one marked
DELETE /api/v1/me/sessions/{id}ends one session
DELETE /api/v1/me/sessionsends every session of the account, the current one included
GET /api/v1/me/blocksthe accounts the account blocked, oldest block first, with their names
PUT /api/v1/me/blocks/{user}blocks an account of the tenant
DELETE /api/v1/me/blocks/{user}lifts a block
GET /api/v1/me/cardthe account's visit card, empty while it has none
PUT /api/v1/me/cardfills in the account's visit card
GET /api/v1/cards/{user}the visit card of an account of the tenant
GET /api/v1/me/notification-preferenceswhether each event notifies the account on each channel
PUT /api/v1/me/notification-preferencesturns events on or off for the account

Authorize

GET /api/v1/oauth/authorize takes response_type=code, client_id, redirect_uri, state, code_challenge, code_challenge_method=S256, and provider, the slug of one of the tenant's providers. A tenant that offers one provider needs no provider. display_name is optional: the name a visitor typed before signing in, in a room whose names are typed by its visitors, which a new account takes instead of the provider's; one that is no display name of up to 32 characters fails the sign-in with invalid_request.

The server keeps the request in vws:once:authorize:<SHA-256 of a new state> for 10 minutes, with a PKCE verifier and a nonce of its own, and redirects to the provider with that state, the nonce and its own code challenge.

For the provider email, it redirects to the client's sign-in page instead, which posts the address and the pending request to POST /api/v1/auth/email. The emailed link leads to the callback, which treats it like a provider's answer.

Callback

  1. The server takes the stored request by state, at most once, and rejects a response whose iss (RFC 9207) is not the provider's.
  2. It exchanges the provider's code with its PKCE verifier and validates the ID token: signature against the provider's keys, issuer, audience, nonce and expiry.
  3. It finds the identity by provider and subject, or creates a registered account, applying the provider's allowed email domains and claim mapping. A new account is named after the request's display_name, else the provider's name; afterwards the account keeps its own name, while the identity keeps the provider's latest (Provider names).
  4. It issues an authorization code: 32 random bytes, kept in vws:once:code:<SHA-256> for 60 seconds, bound to the client, the redirect URI, the code challenge and the account.
  5. It redirects to the client's redirect_uri with code, the client's state and iss.

Failures return to the client's redirect URI with error and error_description (RFC 6749, section 4.1.2.1). An unknown client or redirect URI shows an error page instead and never redirects.

Token

The token endpoint takes form-encoded parameters, as OAuth 2.0 requires.

  • grant_type=authorization_code with code, code_verifier, redirect_uri and client_id. An optional Authorization: Bearer header with an anonymous access token upgrades that account. The exchange creates a session.
  • grant_type=refresh_token. The web client sends the refresh cookie; apps send refresh_token and client_id.

It answers {"access_token": …, "token_type": "Bearer", "expires_in": 900}, plus refresh_token for apps; the web client receives the refresh token as a cookie instead. Errors follow RFC 6749, section 5.2: status 400 with {"error": "invalid_grant", "error_description": …}, or invalid_request, invalid_client and unsupported_grant_type. The OAuth endpoints are the only ones that answer with OAuth errors rather than RFC 9457 problems, because OAuth clients such as AppAuth expect them.

Revoke

POST /api/v1/oauth/revoke ends the session of the refresh token in token, or in the cookie for the web client, and clears the cookie. It answers 200 whether or not the token was valid (RFC 7009).

Tokens

Access tokens

Access tokens are JWTs in the RFC 9068 profile, signed with Ed25519: header alg EdDSA, typ at+jwt and kid, the RFC 7638 thumbprint of the public key.

ClaimValue
isshttps://<primary host of the tenant>
subthe user id
audhttps://<primary host of the tenant>/api
client_idthe client
exp, iatVWS_ACCESS_TOKEN_TTL apart, 15 minutes by default
jtia random id
tidthe tenant id
sidthe session id
kindanonymous, guest or registered
  • Roles, tags and memberships stay out of the token, because they change while it lives; services read them per request, with a short cache.
  • Tokens travel only in the Authorization: Bearer header, never in a URL.
  • The HTTP API and the gateway share one verification: the key named by kid among VWS_SIGNING_KEYS, typ, the signature, exp, iss and aud, a tid equal to the tenant of the request's host, and a session that is not revoked.

Refresh tokens

  • A refresh token is 32 random bytes, sent as base64url. The database keeps its SHA-256 in refresh_token.token_hash.
  • Every use rotates it. The used token gets used_at, a new token points at it through parent_id, and the session's expires_at moves to the new token's expiry: 30 days on the web and 90 days in the apps after the last use, and never past the end of a guest period.
  • A token used again more than 10 seconds after its rotation revokes its session with the reason refresh_reuse, because someone holds a copy. Within those 10 seconds it rotates again instead, so a retry after a lost response does not sign the user out.
  • The web client's cookie is vws_refresh with Secure, HttpOnly, SameSite=Strict and Path=/api/v1/oauth, so that only the token and revoke endpoints receive it. In development over plain http it goes without Secure, which browsers may otherwise ignore there.
  • Apps keep the refresh token in the Keychain or the Keystore.

WebSocket sessions

A realtime connection authenticates with the access token in its first message, Hello. The gateway checks it like the HTTP API, once, when the connection opens; the connection then lasts until it closes, and revoking its session closes it with 4000. The client refreshes an expired token before it connects, and Join names the room, whose policy, bans and capacity the gateway checks for the account:

Room policyAdmits
publicevery account
ssoregistered accounts
members, inviteaccounts with an active membership of the world

A banned account is refused in every room of the world, a room at its capacity refuses the next account, and so does a world whose rooms hold its capacity of accounts; an account already in the world, in another tab or room, still enters. A refusal names its reason, ERROR_CODE_NOT_FOUND, ERROR_CODE_FORBIDDEN, ERROR_CODE_BANNED, ERROR_CODE_ROOM_FULL or ERROR_CODE_WORLD_FULL, and closes with 1008.

Tenant admins, and the admins and owners of a world, kick and ban players of the world's rooms with ModerateUser. A kick ends the account's sessions in the room; a ban, for duration_minutes or for good, adds a ban of the account to the world and ends its sessions in every room of the world. Both go to the audit log as member.kicked and member.banned. The tenant's admins see a world's bans in the administration and lift them (HTTP API), which goes to the audit log as member.unbanned.

GET /api/v1/resolve applies the same rules before the client connects, to the account of the request's access token or, without one, to a visitor without an account. It answers a refusal as 403 with a code: sign_in_required for a visitor without an account or with an anonymous one, members_only for an account without a membership, registered_only for a guest in an sso room, and banned. The client explains the code and offers the tenant's providers where signing in helps. Capacities are checked only when the client joins, because only the room's process knows who is in it.

Web client

  • The access token lives in localStorage under vws.accessToken, the refresh token in the cookie. A tab without an access token signs up anonymously with the display name the visitor entered on the name screen; a tab whose token expires within 30 seconds refreshes it before it connects.
  • Tabs share the cookie, and a refresh token presented again after its rotation ends the session, so tabs refresh one at a time under the Web Lock vws.token-refresh, and a first visit in several tabs signs up once under vws.sign-up. A request that fails with 401 refreshes and retries.
  • When the gateway refuses the token, because its session was revoked while it was still stored, the client refreshes once and connects again; a refused refresh signs the tab out, and it starts over as a new anonymous visitor.
  • The account menu names the account and offers the tenant's providers to anonymous and guest accounts, and logging out or logging out everywhere to the others. Logging out starts the tab over as a new anonymous visitor.
  • A sign-in keeps its PKCE verifier, its state and the page it started on in localStorage under vws.signIn and goes to the authorize endpoint. /auth/callback takes them back, at most once, checks the state and exchanges the code with the current anonymous access token, then returns to the page. They live in localStorage rather than the tab, because an emailed link opens in a new tab; a browser keeps one pending sign-in, the latest.
  • /auth/email asks for the address of a sign-in with the provider email and posts it with the pending request to POST /api/v1/auth/email.

Blocks

An account blocks another account of its tenant where the tenant's blocking flag is on (Tenancy); the rooms then hide the two from each other. A block is a row of user_block, the blocker in user_id and the blocked account in blocked_user_id, and goes with either account.

  • PUT answers 204, also for an account already blocked; 403 while the flag is off, 400 for the caller's own account and 404 for an account the tenant does not have. The blocked account is told nothing and does not see the block in its own list.
  • DELETE answers 204, also when there was no block. Listing and lifting blocks work while the flag is off, when blocks hide nobody.
  • Each block made or lifted is published on vws:topic:blocks, so that every process applies it to its running rooms at once.

Visit cards

An account tells the others of its tenant about itself on a visit card: a headline of up to 100 characters, up to 1,000 characters about itself and up to three links, each a web address of up to 2,000 characters with a label of up to 40 or none. A card is a row of visit_card and goes with its account.

  • PUT /api/v1/me/card keeps the card cleaned: the headline on one line with single spaces, the text trimmed, the links without an address left out. It refuses with 422 text that is too long or holds control characters, and an address that is not http or https with a host. A card left with nothing on it is deleted. The answer is the card as it was kept.
  • GET /api/v1/me/card answers the account's card, or an empty one while it has none, so that a form starts from it. GET /api/v1/cards/{user} answers any account of the tenant the card of another, and 404 while that account has none, or for an account the tenant does not have.
  • Every player in a room carries the address of its card in RoomUser.visit_card_url, and the client opens it from the player list and the video tiles.

Notification preferences

An account chooses which events notify it, event by event and channel by channel; the web client shows them as notifications of the browser. A preference is a row of notification_preference that goes with its account, and an event without one notifies.

CategoryEvent
direct_messagea message in a direct conversation of the account
invitationan invitation to talk
bubble_joinedsomeone joining the account's bubble
conversation_messagea message in the chat of the account's bubble or meeting
meeting_remindera meeting of a connected calendar starting in five minutes (Extensions)

The one channel is browser. GET answers every category on every channel with its status, enabled or disabled. PUT takes items, the preferences to change, and answers every preference after the change; an unknown category, channel or status is refused with 422 and changes nothing.

Sessions

  • A session (auth_session) is one sign-in of one client on one device. It records the user agent, the IP address, when it was created and last used, its expiry, and when and why it ended.
  • Ending a session sets revoked_at and revoked_reason: logout, remote_logout, logout_everywhere, refresh_reuse, guest_ended, merged, account_deleted or suspended.
  • It also writes vws:revoked:<tenant id>:<session id> for one access token lifetime, so the session's access tokens stop working at once, and publishes the session on vws:topic:session-revoked, so every gateway closes its connections.
  • A tenant admin who deletes or suspends an account ends its sessions the same way. A world ban ends no session: the world refuses the banned user's joins and closes their connections there.

Defences

  • CSRF. The refresh cookie is SameSite=Strict and only reaches the OAuth endpoints. Cookie requests to them must also come from the same origin: Sec-Fetch-Site: same-origin where the browser sends it, otherwise an Origin equal to the request's host. Every other endpoint takes the access token from a header, which a cross-site request cannot set.

  • Rate limits, as token buckets under vws:rate:, answer 429 with Retry-After:

    BucketLimit
    anonymous accounts per IP address10 a minute, bursts of 20
    authorize and callback per IP address30 a minute
    token endpoint per IP address60 a minute
    refreshes per session10 a minute
    sign-in links per email address5 an hour
    invite acceptances per IP address10 a minute

    The IP address is the client's as VWS_TRUSTED_PROXIES vouch for it through X-Forwarded-For; without a trusted proxy it is the peer's.

  • Key rotation. Configuration describes rotation through VWS_SIGNING_KEYS; the JWKS lists every configured key. A compromised key is removed at once: its tokens stop verifying, and clients refresh into tokens signed by the remaining key.

Threats

ThreatDefence
A stolen access tokenlives 15 minutes; never in URLs or logs; a strict CSP limits script injection
A stolen refresh tokenout of reach of scripts on the web; rotation with reuse detection ends the session at the second use
CSRF on the cookieSameSite=Strict, the cookie path, and the same-origin check
Open redirectsexact redirect URIs, derived from verified domains on the web
An intercepted authorization codePKCE S256 required; codes live 60 seconds, work once and stay bound to client, redirect URI and challenge
Redirect URI hijacking in the appsclaimed HTTPS links instead of custom schemes, plus PKCE
Identity provider mix-upstate bound to one provider and iss checked on the response
Account takeover through an email claimidentities match on provider and subject only
A token sent over the WebSocketin the first frame rather than the URL; checked when the connection opens; revocation closes the connection
An intercepted sign-in linkworks once, for 15 minutes, and only completes the sign-in that requested it
A flood of anonymous accountsrate limits per IP address, and deletion of accounts unused for 37 days

Sequences

First visit

sequenceDiagram
    participant C as Web client
    participant H as HTTP API
    participant G as Gateway
    C->>H: POST /api/v1/auth/anonymous with a display name
    H-->>C: access token, and the refresh cookie
    C->>H: GET /api/v1/resolve with the page path
    H-->>C: the room
    C->>G: WebSocket, Hello with the access token
    G-->>C: Welcome
    C->>G: Join with the room's id

Web sign-in

sequenceDiagram
    participant C as Web client
    participant H as HTTP API
    participant P as Identity provider
    C->>C: keep a PKCE verifier and state in local storage
    C->>H: navigate to /api/v1/oauth/authorize with client web, provider, state and challenge
    H->>P: redirect with its own state, nonce and challenge
    P-->>H: GET /api/v1/oauth/callback with code and state
    H->>P: exchange the code, validate the ID token
    H-->>C: redirect to /auth/callback with code, state and iss
    C->>H: POST /api/v1/oauth/token, authorization_code, code_verifier, anonymous access token
    H-->>C: access token, and a new refresh cookie
    Note over C,H: the anonymous account is upgraded or merged

App sign-in

sequenceDiagram
    participant A as App
    participant B as System browser
    participant H as HTTP API
    participant P as Identity provider
    A->>B: open /api/v1/oauth/authorize with client ios, state and challenge
    B->>H: authorize
    H->>P: redirect with its own state, nonce and challenge
    P-->>H: callback with code and state
    H-->>B: redirect to https://host/app/callback with code and state
    B-->>A: the universal link opens the app
    A->>H: POST /api/v1/oauth/token, authorization_code, code_verifier
    H-->>A: access token and refresh token
    A->>A: keep the refresh token in the Keychain

Refresh

sequenceDiagram
    participant C as Client
    participant H as HTTP API
    C->>H: a request with an expired access token
    H-->>C: 401
    C->>H: POST /api/v1/oauth/token, refresh_token, from the cookie or the body
    alt the token is current
        H-->>C: new access token and rotated refresh token
        C->>H: the request again
    else the token was used before
        Note over H: the session ends, reason refresh_reuse
        H-->>C: 400 invalid_grant
        Note over C: start over as a first visit or a sign-in
    end

Connect

sequenceDiagram
    participant C as Client
    participant H as HTTP API
    participant R as Redis
    participant G as Gateway
    opt the access token has expired
        C->>H: POST /api/v1/oauth/token, refresh_token
        H-->>C: a fresh access token
    end
    C->>G: Hello with the access token
    G->>G: verify the signature, expiry and tenant
    G->>R: the session is not in vws:revoked
    G-->>C: Welcome
    C->>G: Join with the room's id
    G->>G: check the room's policy, bans and capacity for the account
    G-->>C: RoomSnapshot

Logout

sequenceDiagram
    participant C as Client
    participant H as HTTP API
    participant R as Redis
    participant G as Gateway
    C->>H: POST /api/v1/oauth/revoke with the cookie or the refresh token
    H->>H: end the session, reason logout
    H->>R: SET the session in vws:revoked, PUBLISH session-revoked
    R-->>G: session-revoked
    G-->>C: close the session's connections, 4000
    H-->>C: 200, the cookie cleared

Log out everywhere

sequenceDiagram
    participant C as Client
    participant H as HTTP API
    participant R as Redis
    participant G as Gateways
    C->>H: DELETE /api/v1/me/sessions with the access token
    H->>H: end every session of the account, reason logout_everywhere
    H->>R: SET vws:revoked for each session, PUBLISH each
    R-->>G: session-revoked
    G-->>G: close the connections of those sessions
    H-->>C: 204, the cookie cleared
    Note over C,H: other devices fail their next request and refresh, which is refused

A web client whose connection closes this way starts over; the server refuses its revoked token, its refresh fails, and the tab enters again as a new anonymous visitor.

Found a mistake? Tell us.