> ## Documentation Index
> Fetch the complete documentation index at: https://voyant.travel/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Auth and identity

> Actor types, how authentication is wired, API tokens and scopes, service keys for machine-to-machine, and the token signing baseline.

Authentication in Voyant is shared runtime infrastructure, not a pattern each module reinvents. The platform resolves request identity once, normalizes it, and hands routes and services a consistent auth context. Modules consume that context; they do not own their own auth semantics.

This page explains the actor types, how authentication is wired, the difference between identity and authorization, API tokens and scopes, service-to-service auth, and the token signing baseline.

## 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?                                          |

These are related but distinct. A route should ask for the narrowest thing it actually needs:

* `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.

Do not treat every authenticated route as a permission-checked route, and do not collapse identity, actor selection, and permission checks into one generic guard.

## Actor types

Voyant request context carries an **actor type**: the business class of the principal making the request. The shared `require-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.

Actor type is the business audience. It is separate from the **caller type**, which records the mechanism that authenticated the request (a session, an API key, an internal request). Both are normalized onto the request context so routes and the [action ledger](/docs/platform/fundamentals/events) can record who acted and how.

## 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 platform 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/auth` resolves session, API-key, and shared-secret JWT auth.
* `./middleware/require-actor` enforces the `staff` / `customer` / `partner` / `supplier` actor.
* `./middleware/require-permission` applies permission-based guards.

Routes consume the result through helpers rather than reading raw request state:

```ts theme={null}
import { requireUserId, requireActor } from "@voyant-travel/hono"

adminRoutes.get("/me/preferences", async (c) => {
  const userId = requireUserId(c)
  const prefs = await c.var.container.identityService.getPreferences(userId)
  return c.json({ preferences: prefs })
})
```

<Note>
  Voyant-provisioned admin deployments run in `voyant-cloud` auth mode, where Voyant is an **identity broker**: it owns the login experience, organization membership, app scope, 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. See [Voyant login](/docs/guides/voyant-auth) for the full mechanism, and [Extend auth](/docs/guides/extending-auth) for the integration and Better Auth seams.
</Note>

## 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's `Record<string, string[]>` permission shape:

```ts theme={null}
{
  products: ["read"],
  bookings: ["read", "write"],
  workflows: ["trigger"],
  webhooks: ["relay"],
}
```

Wildcards are supported (`{ "*": ["*"] }`, `{ 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`                       |

So `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.

```bash theme={null}
curl https://api.example.com/v1/public/products \
  -H "Authorization: Bearer voy_..."
```

Tokens are created from the operator Settings screen or the `/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.

<Warning>
  In Cloud mode, a personal API token is still owned by a Cloud-mirrored user. Its use is gated on current Voyant membership: if the user's Voyant membership is revoked, that user's local API keys are disabled. Local tokens remain Better Auth API keys, but Cloud mode revalidates membership before honoring them.
</Warning>

See [Service API keys](/docs/sdks/authentication) and [API routes](/docs/guides/consume-the-api) 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.

Keeping these separate is deliberate. Cookie sessions, API keys, shared-secret bearer claims, and any future machine token are not blurred into one generic "token auth" concept.

## 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-claims` is 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.

Asymmetric signing and JWKS distribution are deferred until there is a real independent verifier surface: multiple services that must verify Voyant-issued tokens without a shared secret, or a non-Voyant consumer that needs public-key verification. Key-rotation pressure alone does not justify the jump; the verifier topology has to require it. When that day comes, the first slice stays narrow: one signing-key abstraction, one rotation policy, one verifier audience.

<Tip>
  Route and module code should consume only the normalized auth context. It should never need to know whether the upstream credential was a session cookie, a `voy_` API key, or a shared-secret bearer token. Keep token-format details inside the shared auth and runtime surfaces.
</Tip>

## Next steps

<CardGroup cols={2}>
  <Card title="API routes" icon="globe" href="/docs/guides/consume-the-api">
    How the auth middleware and guards sit in the route pipeline.
  </Card>

  <Card title="SDK authentication" icon="key" href="/docs/sdks/authentication">
    Authenticate a typed client with sessions or API tokens.
  </Card>

  <Card title="Events" icon="bolt" href="/docs/platform/fundamentals/events">
    The action ledger that records who acted, under which authority.
  </Card>

  <Card title="Architecture" icon="sitemap" href="/docs/concepts/architecture">
    The deployment boundary that backs Voyant's tenancy model.
  </Card>
</CardGroup>
