Skip to main content
A deployment runs its admin surface in exactly one of two mutually exclusive auth modes, selected by the VOYANT_ADMIN_AUTH_MODE environment variable:
  • local (the default): the deployment owns the whole login experience through its own Better Auth flows. This is the right mode for local development and self-hosted deployments.
  • voyant-cloud: the deployment delegates the login experience to Voyant, which acts as the identity broker. This is the mode that Voyant Cloud-provisioned admin deployments run in.
The two modes are resolved in resolveOperatorAuthMode (in the operator starter’s src/api/auth/handler.ts). An empty or missing value resolves to local. Any value that is not exactly "local" or "voyant-cloud" fails closed to voyant-cloud, so a typo never accidentally re-enables the local login flows.
This guide covers the voyant-cloud mode end to end. For the actor model, API tokens, and the normalized auth context that every route consumes regardless of mode, read Auth and identity first.

How it works

In voyant-cloud admin mode, Voyant is an identity broker, not a full replacement for the deployment’s session layer. The model is “trusted assertion to local session mirror,” not “delegate every request to the platform.” The platform owns the human-facing parts of identity; the deployment keeps owning the session it actually issues. Concretely, the platform owns the login UI, organization membership, app scope, the signed grant, and ongoing revalidation. The deployment still owns a local Better Auth session cookie, a local mirror user row, its JWKS and JWT token endpoints, sign-out, and its local API keys. After sign-in, route and module code only ever sees the same normalized { userId, actor, scopes } auth context it sees in local mode. Nothing downstream is aware that Voyant was involved. This is the admin realm only. Storefront customer accounts run in an isolated Better Auth realm with separate storage, cookies, and signing secrets. Managed deployments select adminAuth: "voyant-cloud" and customerAuth: "better-auth"; there is no shared auth selector or signing key between the two realms. Merchant social credentials never pass through WorkOS or the profile settings object. Voyant stores them KMS-encrypted per storefront and environment; internal runtime resolution uses opaque vault:// references, while dashboard and public storefront configuration expose only enabled/configured booleans. Storefront callbacks must use verified HTTPS origins. Customer sessions need a same-origin storefront BFF/proxy with host-only cookies; that delivery layer is not implemented by the control-plane credential store.

Configure storefront customer accounts

The managed Voyant overview includes a Storefront customer accounts card. Choose an experience app and one of its environments, upload write-only Google, Facebook, or Apple credentials, then enable the desired sign-in methods. Email code is the default; email/password and social providers are opt-in. Enabling a social provider fails until that provider has a stored credential bundle. Credential writes return 204 with no response body. Subsequent reads expose only { configured: boolean } per provider, never plaintext, ciphertext, or a credential reference. Replacing a credential uses the same write-only action. For now the dashboard derives the callback origin from the selected experience app’s single active custom hostname. The API independently checks that origin against the active hostname, so a browser cannot assert an arbitrary origin as verified. Apps without an active custom hostname can upload credentials but cannot save the method policy. Supporting multiple verified storefront domains requires a first-class multi-hostname verification model; it must not be added as a free-form origin field. The organization-scoped API contract is rooted at:
GET returns the non-secret projection, PUT saves methods plus the verified origin, and PUT /credentials/:provider replaces a write-only credential. All mutations use the normal Cloud manage permission check. This control-plane UI does not turn the operator origin into a customer-session host. A storefront still needs the same-origin BFF/proxy that resolves its scoped credential references and mounts the Better Auth customer realm on the storefront origin. Here is what each concern resolves to in voyant-cloud mode:

The sign-in flow

The browser sign-in flow is a code-and-exchange round trip between the deployment and Voyant. The deployment issues a signed state cookie, hands off to the platform for authentication, then exchanges a one-time code for a signed assertion it verifies locally before minting its own session. Step by step, verified against packages/auth/src/cloud-broker/state.ts, packages/auth/src/cloud-broker/assertion.ts, packages/auth/src/cloud-admin-session.ts, and the operator application’s src/api/auth/handler.ts:
  1. GET /auth/admin/services/start issues a signed state cookie (an HMAC-SHA256 signature over the state payload, keyed by SESSION_CLAIMS_ADMIN_SECRET) and returns a 302 to VOYANT_CLOUD_ADMIN_AUTH_START_URL carrying deployment_id, redirect_uri, state, nonce, and surface=admin (plus next, and app_id and environment when configured).
  2. The user authenticates on Voyant.
  3. GET /auth/admin/services/callback?code&state runs the Better Auth plugin createVoyantCloudAdminAuthPlugin. It verifies the state cookie against the state query parameter and its expiry, then exchanges the code at VOYANT_CLOUD_ADMIN_AUTH_EXCHANGE_URL (with the deployment client token as a Bearer credential) for a signed assertion in compact JWS form.
  4. The deployment verifies the assertion’s RS256 signature against VOYANT_CLOUD_ADMIN_AUTH_JWKS_URL and validates its claims: iss is https://api.voyant.travel, aud matches the configured audience, deploymentId matches this deployment, nonce matches the state, and exp is in the future.
  5. It upserts a local mirror user (with a local UUID, not the broker user id) plus a link row that stores the broker ids, deployment id, role, and member scopes. It then creates a local Better Auth session, sets the session cookie, and redirects to the validated next path.
From this point on, the deployment authenticates the user with its own session cookie. The assertion was a one-time proof of identity, not a long-lived token.

Configure cloud mode

Cloud mode needs the broker endpoints, the deployment’s identity, and the cookie signing secret. The operator application declares these in env.d.ts: The storefront customer realm uses its own SESSION_CLAIMS_CUSTOMER_SECRET and BETTER_AUTH_CUSTOMER_SECRET; neither is accepted by the admin realm.
Voyant-provisioned deployments receive these values from the platform automatically. A self-hosted deployment needs none of them: it stays in local mode and uses the regular Better Auth flows.
If VOYANT_ADMIN_AUTH_MODE is voyant-cloud but the exchange or revalidate configuration is incomplete, the cloud routes respond with a clear “not configured” status rather than silently falling back to local login, since the mode itself fails closed.

Revalidation and revocation

A local session cookie outlives a single platform decision, so cloud mode revalidates access against the platform on an interval rather than trusting the cookie indefinitely. On each protected request, the deployment runs a revalidation step keyed on the session. If the session was revalidated within the cached interval, there is no network call. Otherwise the deployment makes a POST to VOYANT_CLOUD_ADMIN_AUTH_REVALIDATE_URL. When the platform reports active, the deployment refreshes the cached interval and, if the platform sent an updated scope set, the member’s cached RBAC scopes. That is why a permission change made on the platform takes effect within one interval rather than only at next login. When the platform reports revoked, the request is rejected and the session is torn down. The default interval is roughly 15 minutes (DEFAULT_CLOUD_SESSION_REVALIDATE_AFTER_SECONDS in packages/auth/src/cloud-admin-session.ts). Personal voy_ API tokens are revalidated the same way in cloud mode. The token validation hook re-checks the owning user’s platform membership, so a token stops authenticating once that user’s Voyant access is revoked, even though the token itself is still a local Better Auth API key.
Revocation latency is bounded by the revalidation interval, not instant. A user whose platform access is revoked can keep using a cached session or token until the next revalidation, up to roughly 15 minutes later.

Team and invitations

Membership lives on the platform in cloud mode, so the deployment does not manage its own roster. The operator application mounts a cloud-only team surface at /v1/admin/team that proxies to the platform member API. It exposes listing members, listing roles, listing and creating and revoking invitations, setting a member’s deployment access, and setting a member’s per-deployment permissions (the resource:action scope set). These routes return 404 in local mode. Every proxied call carries the deployment client token, the deployment id, and the acting staff user’s broker id (resolved from the local link row). The platform re-verifies that the acting user is an organization manager before mutating anything, so the deployment never self-asserts authority over the roster. In local mode the equivalent capability is the deployment’s own /v1/admin/invitations super-admin flow, which issues and redeems invitations against the local user table. The two are mutually exclusive: the local invitations surface is the local-mode path, and the team proxy is the cloud-mode path.

What changes in cloud mode

Switching a deployment into voyant-cloud mode reshapes the local auth surface:
  • The Better Auth surface is reduced to an allowlist. Only /auth/admin/get-session, /auth/admin/jwks, /auth/admin/session, /auth/admin/sign-out, and /auth/admin/token pass through to Better Auth, alongside the cloud-only /auth/admin/services/start and /auth/admin/services/callback routes. Managed operator deployments expose the same routes under /api/auth/admin/*. Everything else returns a “local auth disabled” response.
  • Local self-service flows are blocked. Sign-up, password reset, OAuth, email verification, and local invitation redemption do not run in the deployment. Those concerns belong to the platform.
  • The mirror-user invariant holds. The local user.id is a freshly minted UUID, deliberately not the broker user id. The broker ids are kept on a separate link row. If you join other records on a user id, join on the local user.id, never on the broker id.
Because module code only ever consumes the normalized auth context, none of this changes how you author routes. A route that calls requireUserId(c) behaves identically in both modes; only the machinery that produced the session differs.

Next steps

Auth and identity

Actor types, the normalized auth context, API tokens, and scopes.

Extend auth

Wire a different provider or add behavior behind the auth seam.

Voyant

What the platform provisions and operates for a deployment.

SDK authentication

Authenticate a typed client with sessions or API tokens.