Every WebTransport session is authenticated by a bearer JWT minted by WorkOS, the platform’s identity provider. The SDK presents the token once, on the first frame of the control stream; the platform validates it and binds the session to the principal and tenant the token names. Tokens are short-lived — the SDK refreshes them out-of-band and opens a new session when an old one’s token nears expiry.Documentation Index
Fetch the complete documentation index at: https://docs.fluxamerica.io/llms.txt
Use this file to discover all available pages before exploring further.
The model in one paragraph
WorkOS is the identity provider. The platform never sees a password — only a signed access token. On every session the platform validates the token’s signature against WorkOS’s JWKS, checks the standard OIDC claims (iss, aud, exp, sub, org_id), and resolves org_id to a platform tenant. Tokens are short-lived (minutes, not hours); the WorkOS device flow returns both an access token and a refresh token, and the SDK uses the refresh token to mint replacements without re-prompting the user.
Discovery: /.well-known/auth-config
The SDK does not hard-code the WorkOS endpoints, the OAuth client_id, or the audience. It fetches them from the platform at startup:
auth_provider— IdP family identifier. Always"workos"in v1; the field exists so a future ADR can add others.device_authorization_endpoint— RFC 8628 device-authorization URL the SDK POSTs to in order to start a login.token_endpoint— token URL the SDK polls during device flow and later hits to redeem a refresh token.client_id— OAuth client identifier the SDK presents to the IdP. Not a secret.scopes— OIDC scopes the SDK must request.offline_accessis what makes the IdP issue a refresh token.audience— the access token’s intended audience. The platform rejects tokens whoseaudclaim does not contain this value.platform_base_url— public base URL the SDK dials for the WebTransport listener once it has a token.
login.
Obtaining a token
The SDK runs the WorkOS device-authorization flow (RFC 8628):- Start the flow. POST to
device_authorization_endpointwith theclient_idandscopesfrom discovery. The response contains adevice_code, auser_code, averification_uri_complete(the URI with the user code pre-filled), anexpires_in, and aninterval. - Surface the URI to the user. What the SDK does here is out of scope — open a browser, print to a terminal, render a QR code. The platform does not care.
- Poll the token endpoint. Every
intervalseconds, POST totoken_endpointwithgrant_type=urn:ietf:params:oauth:grant-type:device_code, thedevice_code, and theclient_id. The endpoint repliesauthorization_pendinguntil the user completes verification; on success it returns an access token, a refresh token, anexpires_in, and atoken_typeofBearer. - Store the tokens. The access token is what the SDK sends on the handshake (next section). The refresh token persists across access-token expiries and should be stored durably.
token_endpoint with grant_type=refresh_token and the refresh token; the response includes a fresh access token and usually a rotated refresh token, which the SDK persists in place of the old one.
The handshake
Once the SDK has an access token, the session itself is a WebTransport connection toplatform_base_url. The full framing is in the Protocol reference § 4; the auth-specific contract is:
- The SDK opens the QUIC + WebTransport connection. TLS 1.3 with standard PKI; the platform’s certificate is browser-trusted.
- The SDK opens exactly one bidirectional control stream.
- The SDK sends a
ClientHelloframe as the first Postcard-framed payload on that stream. The frame carries the access token in itsauthfield (as a WorkOS-signed JWT — noBearerprefix, the wire format is a raw token string), along with the SDK’s preferred protocol versions, its capability set, and identifying metadata (product name, version). - The platform validates the token (next section), picks a mutually-supported protocol version, intersects capabilities, and replies with
ServerHello— containing the negotiated protocol version, the negotiated capabilities, an opaquesession_idfor log correlation, server metadata, andauth_expires_at(theexpclaim from the bound token, exposed so the SDK does not have to crack the JWT itself).
OpError::Auth(...) on the control stream and closes the session. There is no in-session retry — the SDK must reconnect with a different (typically fresh) token.
The handshake has a 5-second wall-clock budget. A session that does not complete the handshake in that window is torn down; the SDK may receive OpError::Auth(HandshakeTimeout) on a best-effort basis (or no frame at all, if the tear-down beats the write).
Claims the platform validates
The platform reads the standard OIDC claims plus WorkOS’sorg_id. The SDK does not need to validate signatures locally — the platform does that — but it should know what shape it is sending, so it can pre-validate cheaply (for example, refusing to send an obviously expired token):
| Claim | What it means | Reject reason |
|---|---|---|
iss | Issuer URL the token was minted by. Must match the platform’s configured WorkOS issuer. | Auth(TokenInvalid) on mismatch. |
aud | Audience. Must contain the audience value the discovery endpoint advertised. | Auth(TokenInvalid) on mismatch. |
exp | Expiry, Unix seconds. | Auth(TokenExpired) when exp is in the past. |
sub | Subject — the WorkOS user id. | Auth(TokenInvalid) if missing. |
org_id | WorkOS organization id. Resolves 1:1 to a platform tenant. | Auth(TokenInvalid) if missing; the first session from an unknown org_id triggers tenant provisioning, which can itself fail with an internal error. |
RS256 and carry a kid header that names a key in the WorkOS JWKS. The platform fetches and caches the JWKS server-side; the SDK never reads it directly.
Beyond the table above, tokens may carry a permissions claim (an array of permission name strings). It is optional; tokens without it are treated as carrying an empty permission set. Privileged ops (currently SubscribeAuditTail, requiring audit_tail.read) are rejected with Auth(PrincipalLacksPermission { required }) when the bound principal’s permission set does not include the required name.
Token expiry mid-session
Tokens expire while sessions are open. The canonical rule:- Ops already in flight when
exppasses are completed on a best-effort basis. The platform does not abort them mid-stream. - New op streams opened after
expare rejected withOpError::Auth(TokenExpired). The platform checks expiry at op-stream creation, not per frame. - The platform does not rotate the bound token in-session. There is no
TokenRefreshcontrol frame in v1.
- Read
auth_expires_atfrom theServerHello. (Equivalent to theexpclaim on the bound token; surfaced for convenience.) - Before
auth_expires_at, mint a replacement token via the refresh-token flow above. - When the existing session refuses a new op stream with
Auth(TokenExpired)— or proactively, ahead of the deadline — open a new WebTransport session with the fresh token. Long-running subscriptions can survive the cutover by resuming on the new session with the resume token they were last given (see the Protocol reference § 6.2 for resume semantics).
TokenExpired.”
Error responses
Auth-related errors all flow as theOpError::Auth(AuthErrorKind) variant. The SDK should handle each one explicitly:
TokenInvalid— signature failed,issorauddid not match,subororg_idwas missing or malformed, or the token was otherwise unparseable. The SDK should treat this as a hard failure: do not retry with the same token. If it occurred at handshake time, prompt the user to re-authenticate.TokenExpired—expis in the past. The SDK should refresh the access token and open a new session. This is the common, expected case during normal operation.PrincipalLacksPermission { required }— authentication succeeded but the principal does not hold the named permission claim. Therequiredfield is the permission name (e.g.audit_tail.read). Surface this to the user; do not retry, the answer will not change without IdP-side configuration.HandshakeTimeout— the handshake did not complete within the platform’s 5-second budget. Usually a network problem, not an auth problem; retry with backoff.ProtocolVersionUnsupported— none of the protocol versions the SDK offered inClientHello.supported_protocol_versionsis supported by the platform. The SDK is too old (or, briefly, too new) for the deployed platform; the only resolution is an SDK update.
PreconditionFailed(CapabilityNotNegotiated) error, which is not an auth error but is also commonly encountered at handshake time.
Compatibility commitments
The auth surface is a stability contract:- The discovery JSON shape is wire-stable. New fields land at the end; existing fields never change name, type, or meaning. SDKs should ignore unknown fields so server roll-outs do not break older clients.
- JWT claim names are wire-stable. The five claims in the table above are required; additions are additive.
- The handshake frames (
ClientHello,ServerHello) are protocol-versioned. Their layout is covered by the negotiatedprotocol_version(ADR-0023); a major-version bump is the only mechanism that can change them. - The 5-second handshake budget and the “expiry rejects new op streams” rule are part of the wire contract per the Protocol reference § 2.7. They will not silently change between minor versions.