Identity is not authorization
Voyant keeps three concerns separate, and conflating them is the most common auth mistake.| Concern | Question it answers |
|---|---|
| Identity | Who is the user? |
| Actor context | Which workspace or organization do they act in, and as what kind of actor? |
| Permission | What are they allowed to do here? |
requireUserId(...)for “must be signed in.”- Actor-aware middleware when the route genuinely depends on workspace or actor context.
- An explicit permission check when the route depends on a specific grant.
Actor types
Voyant request context carries an actor type: the business class of the principal making the request. The sharedrequire-actor middleware enforces one of:
staff: operator and back-office users on the admin surface.customer: end travelers on the public surface.partner: external partners with their own scoped access.supplier: upstream suppliers feeding inventory or fulfilment.
How authentication is wired
Session-based auth is the primary default for admin and authenticated public surfaces. In the first-party starters that means Better Auth: it owns session issuance, the session cookie, the user table, and the JWT/JWKS endpoints. The core framework packages stay provider-agnostic. They expect request auth to be resolved already and consume a normalized{ userId, actor, scopes } context, so a deployment can wire a different provider without rewriting module code.
The Hono middleware chain runs auth handler → requireAuth → ... → actor guards before any module route. @voyant-travel/hono exposes the pieces:
./middleware/authresolves session, API-key, and shared-secret JWT auth../middleware/require-actorenforces thestaff/customer/partner/supplieractor../middleware/require-permissionapplies permission-based guards.
Voyant Cloud-provisioned admin deployments run in
voyant-cloud auth mode, where Voyant Cloud is an identity broker: it owns WorkOS identity, organization membership, app scope, broker grants, and revalidation, while the tenant deployment still owns its local Better Auth mirror user, session cookie, and local token storage. In that mode, local sign-up, password reset, OAuth, and invitations are disabled server-side. Local development and self-host run in local mode with the regular Better Auth flows. Either way, route and module code only ever sees the normalized auth context.Identity storage
User identity and profile data is a shared capability that both admin and public surfaces can read and update safely: locale, timezone, contact and billing preferences, and traveler identity data where appropriate. One subtlety worth keeping straight: admin UI locale is a user preference, not the same concern as product-content translation. The identity surface stores user-level locale and timezone so the runtime can resolve presentation; it does not own content translation.API tokens and scopes
API tokens are for automation and cross-runtime integrations: CMS sync jobs, storefront proxies, webhook relays, workflow triggers. They are not operator sessions, and they must never be promoted into one. Under the hood, an API token is a Better Auth API key. Better Auth owns creation, hashing, storage, listing, rotation, and deletion. The authorization model is Better Auth’sRecord<string, string[]> permission shape:
{ "*": ["*"] }, { products: ["*"] }, { "*": ["read"] }). The catalog is intentionally extensible: a new module uses its own name as the resource without a central enum change, though common permissions are added to the shared descriptor catalog (@voyant-travel/types/api-keys) so the management UI can display them.
How the route resource maps to a permission
For API-key callers,requireActor(...) derives the required permission from the URL and HTTP method. It takes the first path segment after /v1/admin/ or /v1/public/ as the resource and the method as the action:
| Method | Derived action |
|---|---|
GET / HEAD | read or search |
POST | write, trigger, or relay |
PUT / PATCH | write |
DELETE | delete |
GET /v1/public/products is admitted by { products: ["read"] } (or a matching wildcard), and POST /v1/admin/workflows/events is admitted by { workflows: ["trigger"] }. This is why route URLs keep their resource segment aligned with the permission you expect third-party callers to hold.
When a voy_ token authenticates, the middleware sets callerType: "api_key", the token id, and the resolved scopes on the context. Audit consumers record the token id, never the raw bearer secret.
/auth/api-tokens management facade, the returned secret is shown once, and a token can be rotated in place (new secret, same id, permissions, and counters) so the previous secret stops authenticating immediately.
See Service API keys and API routes for how this plays out at the route layer.
Machine-to-machine and internal auth
Machine and internal auth is kept distinct from user-session auth. Voyant does not overload the session model to cover every non-user access pattern. There are a few separate primitives, and they are not interchangeable:- API keys (
voy_bearer tokens), for external automation and cross-runtime integrations, validated against Better Auth permissions as above. - Internal requests, marked on the context (
isInternalRequest) and admitted past the actor guard for trusted in-deployment callers. - Shared-secret bearer claims through
@voyant-travel/utils/session-claims, for short-lived internal bearer tokens and runtime-local verification without a database round trip. This produces a minimal normalized{ userId, sessionId }claim set.
Token signing and key distribution
The current baseline is intentionally narrow, and the docs are explicit about not pretending Voyant is already a JWKS-distributed token platform.- Better Auth session cookies remain the primary user-session model. Voyant does not replace them with a JWT-first architecture.
session-claimsis a narrow symmetric helper: HMAC-SHA256, short-lived, minimal payload, verified locally through Web Crypto. It is a runtime convenience, not a platform-wide token standard.- Edge-sensitive code may verify identity differently (for example a direct session-table check) when runtime constraints differ, as long as the resulting auth context is normalized the same way.
Next steps
API routes
How the auth middleware and guards sit in the route pipeline.
SDK authentication
Authenticate a typed client with sessions or API tokens.
Events
The action ledger that records who acted, under which authority.
Architecture
The deployment boundary that backs Voyant’s tenancy model.