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.
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
Invoyant-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 return204 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-timecode 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:
GET /auth/admin/services/startissues a signed state cookie (an HMAC-SHA256 signature over the state payload, keyed bySESSION_CLAIMS_ADMIN_SECRET) and returns a302toVOYANT_CLOUD_ADMIN_AUTH_START_URLcarryingdeployment_id,redirect_uri,state,nonce, andsurface=admin(plusnext, andapp_idandenvironmentwhen configured).- The user authenticates on Voyant.
GET /auth/admin/services/callback?code&stateruns the Better Auth plugincreateVoyantCloudAdminAuthPlugin. It verifies the state cookie against thestatequery parameter and its expiry, then exchanges thecodeatVOYANT_CLOUD_ADMIN_AUTH_EXCHANGE_URL(with the deployment client token as a Bearer credential) for a signed assertion in compact JWS form.- The deployment verifies the assertion’s
RS256signature againstVOYANT_CLOUD_ADMIN_AUTH_JWKS_URLand validates its claims:issishttps://api.voyant.travel,audmatches the configured audience,deploymentIdmatches this deployment,noncematches the state, andexpis in the future. - 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
nextpath.
Configure cloud mode
Cloud mode needs the broker endpoints, the deployment’s identity, and the cookie signing secret. The operator application declares these inenv.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.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 aPOST 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.
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 intovoyant-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/tokenpass through to Better Auth, alongside the cloud-only/auth/admin/services/startand/auth/admin/services/callbackroutes. 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.idis 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 localuser.id, never on the broker id.
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.