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.kind | Created by | Signs in with | Ends |
|---|---|---|---|
anonymous | the first visit of a client without a session | its session only | 7 days after its last session expired |
guest | accepting a world invite sent by email, or opening a scheduled meeting's link | its session only | at the end of the guest period, or when the meeting's link expires |
registered | the first sign-in with an identity provider | any identity linked to it | when 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 roleguest. Access lasts the tenant'sguestLifetime, 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 planstandard, so guests cannot stand in for paid accounts, or more as extra charges where its admins allow them (Tenancy). World invites need theguestsflag (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 withGET /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}/meetingsschedules 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 theguestsflag 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}/meetingslists the room's meetings that have not ended, for its world's members, andDELETE /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_providerrow 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
identityProviderssetting lists the providers it offers, in order; without it the tenant offers its own providers, then the platform's, thenemail. The product stores no passwords. - The provider
emailsigns 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_KEYand 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.GETlists 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, thenemail, each with where it comes from.POSTadds a provider with its slug, name, issuer, client id and client secret, and optionally its scopes (openid email profileby default,openidrequired), allowed email domains, name claim (nameby default), tags claim, logo and status (activeby default);PUT .../{provider}changes one, keeping the stored secret when the body has none;DELETEremoves 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, andemailanddevelopmentare reserved. No answer contains the client secret, sealed or not, and every change goes to the audit log asidentity_provider.created,identity_provider.updatedoridentity_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/callbackfor 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
developmentsigns 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 ofapi/openapi.yaml. - The provider
emailis offered whenVWS_EMAIL_BACKENDis notoff. In development thelogbackend writes the link to the server's log instead of sending it.
Clients
| Client | auth_client.kind | Redirect URI | Refresh token |
|---|---|---|---|
web | web | https://<host>/auth/callback for every verified host of the tenant | an httpOnly cookie |
ios, android | ios, android | https://<host>/app/callback, claimed by the app as a universal link or app link | the 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_idset) with their own redirect URIs. The tenant's hosts serve/.well-known/apple-app-site-associationand/.well-known/assetlinks.jsonfor 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
| Endpoint | Purpose |
|---|---|
GET /.well-known/oauth-authorization-server | RFC 8414 metadata: the issuer, the endpoints, code responses, the authorization_code and refresh_token grants, S256, public clients |
GET /.well-known/jwks.json | the public keys that verify access tokens |
GET /api/v1/auth/providers | the identity providers the tenant offers: slug, name, logo |
GET /api/v1/oauth/authorize | starts a sign-in |
GET /api/v1/oauth/callback | the identity provider's return |
POST /api/v1/oauth/token | exchanges a code, or refreshes |
POST /api/v1/oauth/revoke | ends the session of a refresh token (RFC 7009) |
POST /api/v1/auth/anonymous | creates an anonymous account and its session |
POST /api/v1/auth/email | sends 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}/accept | creates a guest account and its session, for a world invite or a meeting link |
GET /api/v1/me | the account of the access token |
GET /api/v1/me/sessions | the account's sessions, the current one marked |
DELETE /api/v1/me/sessions/{id} | ends one session |
DELETE /api/v1/me/sessions | ends every session of the account, the current one included |
GET /api/v1/me/blocks | the 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/card | the account's visit card, empty while it has none |
PUT /api/v1/me/card | fills 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-preferences | whether each event notifies the account on each channel |
PUT /api/v1/me/notification-preferences | turns 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
- The server takes the stored request by state, at most once, and rejects a response whose
iss(RFC 9207) is not the provider's. - 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.
- 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). - 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. - It redirects to the client's
redirect_uriwithcode, the client'sstateandiss.
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_codewithcode,code_verifier,redirect_uriandclient_id. An optionalAuthorization: Bearerheader 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 sendrefresh_tokenandclient_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.
| Claim | Value |
|---|---|
iss | https://<primary host of the tenant> |
sub | the user id |
aud | https://<primary host of the tenant>/api |
client_id | the client |
exp, iat | VWS_ACCESS_TOKEN_TTL apart, 15 minutes by default |
jti | a random id |
tid | the tenant id |
sid | the session id |
kind | anonymous, 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: Bearerheader, never in a URL. - The HTTP API and the gateway share one verification: the key named by
kidamongVWS_SIGNING_KEYS,typ, the signature,exp,issandaud, atidequal 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 throughparent_id, and the session'sexpires_atmoves 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_refreshwithSecure,HttpOnly,SameSite=StrictandPath=/api/v1/oauth, so that only the token and revoke endpoints receive it. In development over plain http it goes withoutSecure, 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 policy | Admits |
|---|---|
public | every account |
sso | registered accounts |
members, invite | accounts 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
localStorageundervws.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 undervws.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
localStorageundervws.signInand goes to the authorize endpoint./auth/callbacktakes 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 inlocalStoragerather than the tab, because an emailed link opens in a new tab; a browser keeps one pending sign-in, the latest. /auth/emailasks for the address of a sign-in with the provideremailand posts it with the pending request toPOST /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.
PUTanswers 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.DELETEanswers 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/cardkeeps 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 nothttporhttpswith a host. A card left with nothing on it is deleted. The answer is the card as it was kept.GET /api/v1/me/cardanswers 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.
| Category | Event |
|---|---|
direct_message | a message in a direct conversation of the account |
invitation | an invitation to talk |
bubble_joined | someone joining the account's bubble |
conversation_message | a message in the chat of the account's bubble or meeting |
meeting_reminder | a 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_atandrevoked_reason:logout,remote_logout,logout_everywhere,refresh_reuse,guest_ended,merged,account_deletedorsuspended. - 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 onvws: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=Strictand only reaches the OAuth endpoints. Cookie requests to them must also come from the same origin:Sec-Fetch-Site: same-originwhere the browser sends it, otherwise anOriginequal 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 withRetry-After:Bucket Limit anonymous accounts per IP address 10 a minute, bursts of 20 authorize and callback per IP address 30 a minute token endpoint per IP address 60 a minute refreshes per session 10 a minute sign-in links per email address 5 an hour invite acceptances per IP address 10 a minute The IP address is the client's as
VWS_TRUSTED_PROXIESvouch for it throughX-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
| Threat | Defence |
|---|---|
| A stolen access token | lives 15 minutes; never in URLs or logs; a strict CSP limits script injection |
| A stolen refresh token | out of reach of scripts on the web; rotation with reuse detection ends the session at the second use |
| CSRF on the cookie | SameSite=Strict, the cookie path, and the same-origin check |
| Open redirects | exact redirect URIs, derived from verified domains on the web |
| An intercepted authorization code | PKCE S256 required; codes live 60 seconds, work once and stay bound to client, redirect URI and challenge |
| Redirect URI hijacking in the apps | claimed HTTPS links instead of custom schemes, plus PKCE |
| Identity provider mix-up | state bound to one provider and iss checked on the response |
| Account takeover through an email claim | identities match on provider and subject only |
| A token sent over the WebSocket | in the first frame rather than the URL; checked when the connection opens; revocation closes the connection |
| An intercepted sign-in link | works once, for 15 minutes, and only completes the sign-in that requested it |
| A flood of anonymous accounts | rate 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.