@voyant-travel/auth
v0.150.5
Published
Auth package for the workspace
Downloads
28,060
Readme
@voyant-travel/auth
Better Auth wiring for Voyant's reference template stack. Provides server-side auth helpers, edge runtime variants, backend utilities, and a permissions contract.
Install
pnpm add @voyant-travel/auth better-authUsage
import {
createBetterAuth,
handleAccountProfileRequest,
handleApiTokenManagementRequest,
} from "@voyant-travel/auth/server"
const auth = createBetterAuth({
db,
secret: env.AUTH_SECRET,
trustedOrigins: ["https://example.com"],
user: {
additionalFields: {
surfaces: {
type: "string",
required: false,
},
},
},
})
const profileResponse = await handleAccountProfileRequest(request, auth, { db })
if (profileResponse) return profileResponse
const tokenResponse = await handleApiTokenManagementRequest(request, auth)
if (tokenResponse) return tokenResponse
return auth.handler(request)Auth provider wiring is starter-owned — core Voyant packages only depend on
the normalized { userId, actor } contract, not on Better Auth specifically.
Local Team Access
Local team deactivation is durable auth state. It revokes the member's current sessions and API keys, blocks cached-session reuse and new Better Auth sessions, and suppresses email OTP flows for the deactivated address. Reactivation restores the member's sign-in providers so new password, social, or OTP sessions can be created; previously revoked sessions and API keys remain revoked.
Local role changes, activation, and deactivation require an interactive transaction. Owner-removing mutations serialize and recheck the active-owner count inside that transaction, so concurrent requests cannot remove the last active owner. Deployments mounting the team API must therefore select a transaction-capable database adapter.
createBetterAuth forwards Better Auth's user options, including
user.additionalFields, while preserving Voyant's default change-email support.
When using additional user fields with the Drizzle adapter, the consuming app is
responsible for adding matching columns and migrations to the auth user table.
By default, createBetterAuth keeps Voyant's single-tenant guard for admin
signups: once any user exists, another user without explicit surfaces, or with
the admin surface, cannot self-register. Customer-facing auth plugins can
still create users by setting a non-admin surface such as storefront.
The guarded surfaces are privileged signup surfaces. The default privileged
surface is admin; pass disableSignupWhenUsersExist.surfaces if a deployment
uses a different staff/admin surface name.
const auth = createBetterAuth({
db,
user: {
additionalFields: {
surfaces: {
type: "string",
required: false,
input: true,
},
},
},
disableSignupWhenUsersExist: {
surfaces: ["admin"],
},
})Better Auth OTP signup flows can apply user.additionalFields defaults before
the package signup guard runs. If surfaces defaults to ["admin"], customer
OTP signups would otherwise be classified as privileged users and rejected once
an admin user exists. Configure customerSignupSurfaces to make
createBetterAuth stamp the supported customer self-signup endpoints before the
guard evaluates the new user:
const auth = createBetterAuth({
db,
user: {
additionalFields: {
surfaces: {
type: "string",
required: false,
input: false,
defaultValue: ["admin"],
},
},
},
customerSignupSurfaces: ["storefront"],
disableSignupWhenUsersExist: {
surfaces: ["admin"],
},
})customerSignupSurfaces applies to Better Auth customer self-signup routes that
create a user during OTP verification, including phone-number verification and
email OTP sign-in. It does not change regular admin sign-up or invitation flows.
Better Auth server plugins that define their own tables must pass those Drizzle
tables through extraSchema so the shared Drizzle adapter can resolve them:
import { createBetterAuth } from "@voyant-travel/auth/server"
import { authInvitation, authMember, authOrganization } from "@voyant-travel/db/schema/iam"
import { organization } from "better-auth/plugins"
const auth = createBetterAuth({
db,
plugins: [organization()],
extraSchema: {
organization: authOrganization,
member: authMember,
invitation: authInvitation,
},
})The app that mounts the plugin owns the plugin migrations. extraSchema only
connects existing Drizzle table definitions to Better Auth; it does not create
or run migrations.
The package also exposes a narrow shared-secret bearer-token helper surface via
@voyant-travel/utils/session-claims for runtime-local verification. That helper is
not a replacement for Better Auth session cookies and does not imply a
platform-wide JWKS/JWT-first auth model.
Voyant Cloud Admin Auth
Voyant Cloud deployments can use @voyant-travel/auth/cloud-admin-session to keep
Better Auth as the local session and JWT/JWKS issuer while delegating identity
and membership checks to Voyant Cloud.
Cloud mode is exclusive. A deployment running with
VOYANT_ADMIN_AUTH_MODE=voyant-cloud should expose only the Cloud start and
callback routes plus the Better Auth endpoints that remain local session/token
infrastructure (get-session, session, sign-out, token, jwks, and API
token management when Cloud revalidation is configured). Local sign-in,
sign-up, invitation redemption, password reset, email verification, email OTP,
change-email, and social OAuth routes must stay server-blocked in Cloud mode.
Local/self-host development should set VOYANT_ADMIN_AUTH_MODE=local and keeps
the normal Better Auth flows.
import { createVoyantCloudAdminAuthPlugin } from "@voyant-travel/auth/cloud-admin-session"
import { createBetterAuth } from "@voyant-travel/auth/server"
const auth = createBetterAuth({
db,
basePath: "/auth/admin",
plugins: [
createVoyantCloudAdminAuthPlugin({
db,
cookieSecret: env.SESSION_CLAIMS_ADMIN_SECRET,
exchange: {
exchangeUrl: env.VOYANT_CLOUD_ADMIN_AUTH_EXCHANGE_URL,
deploymentId: env.VOYANT_CLOUD_DEPLOYMENT_ID,
clientToken: env.VOYANT_CLOUD_ADMIN_AUTH_CLIENT_TOKEN,
assertionJwksUrl: env.VOYANT_CLOUD_ADMIN_AUTH_JWKS_URL,
assertionAudience: env.VOYANT_CLOUD_ADMIN_AUTH_AUDIENCE ?? env.VOYANT_CLOUD_DEPLOYMENT_ID,
},
onUserProvisioning: async ({ db, assertion, user, isNewUser }) => {
// Optional Cloud-mode side effects or app-owned profile/user columns.
// Better Auth databaseHooks.user.create.* do not fire for this trusted
// mirror path because it bypasses public sign-up.
},
}),
],
})The plugin mounts Better Auth's /auth/admin/cloud/callback endpoint. That endpoint
validates the signed broker state, exchanges the one-time Cloud code, verifies
the signed Cloud assertion, upserts the local mirror user/account/profile,
stores Cloud linkage side-table rows, and then creates the local Better Auth
session cookie through Better Auth's own session and cookie helpers.
Cloud mode is intentionally a direct mirror provisioning path, similar to a
trusted invitation redemption path. Consumer Better Auth
databaseHooks.user.create.* are not invoked. Use onUserProvisioning for
Cloud-mode custom fields or side effects.
The mirror user uses a generated local Better Auth user.id; WorkOS user ids
are stored in local account/linkage rows and must not be used as JWT sub.
Cloud linkage metadata lives in cloud_auth_user_links and
cloud_auth_session_links, not in the Better Auth session response shape.
For ongoing access, call revalidateVoyantCloudAdminAuthSession(...) before
Cloud-mode browser-session-sensitive operations and
revalidateVoyantCloudAdminAuthUser(...) for local API-token callers. A revoked
Cloud membership marks the Cloud link revoked and disables local Better Auth API
keys for that mirrored user. Current v1 revalidation is pull/cached; a Cloud
webhook can later reduce revocation latency but should not replace pull checks.
Voyant Cloud-provisioned deployments receive these settings from Cloud:
VOYANT_ADMIN_AUTH_MODE=voyant-cloud
VOYANT_CLOUD_ADMIN_AUTH_START_URL=https://dash.voyantcloud.com/admin-auth/start
VOYANT_CLOUD_ADMIN_AUTH_EXCHANGE_URL=https://api.voyant.travel/cloud/v1/admin-auth/exchange
VOYANT_CLOUD_ADMIN_AUTH_JWKS_URL=https://api.voyant.travel/.well-known/admin-auth/jwks.json
VOYANT_CLOUD_ADMIN_AUTH_REVALIDATE_URL=https://api.voyant.travel/cloud/v1/admin-auth/revalidate
VOYANT_CLOUD_ADMIN_AUTH_AUDIENCE=dep_...
VOYANT_CLOUD_ADMIN_AUTH_CLIENT_TOKEN=...
VOYANT_CLOUD_DEPLOYMENT_ID=dep_...Self-hosted deployments do not need WorkOS or Voyant Cloud configuration.
API Token Management
Better Auth's API Key plugin owns token storage and verification. Voyant adds a
small /auth/api-tokens facade for operator management UI because the UI needs
server-only plugin fields such as permissions, remaining, and enabled.
Mount handleApiTokenManagementRequest(...) before falling through to
auth.handler(request).
In Cloud mode, API-token management and raw voy_ API-token request handling
must both call Cloud revalidation. This prevents a personal API token from
outliving the user's WorkOS/Voyant Cloud membership.
Account Profile
Voyant auth UIs use PATCH /auth/me to update the signed-in user's basic
profile fields: firstName, lastName, locale, timezone, and
profilePictureUrl. Mount handleAccountProfileRequest(...) before falling
through to auth.handler when the app wants shared onboarding or account UI to
submit directly to the auth facade.
updateCurrentUserProfile(db, { userId, ...patch }) from
@voyant-travel/auth/workspace updates the Voyant profile row and returns the
normalized CurrentUser.
Exports
| Entry | Description |
| --- | --- |
| . | Barrel re-exports |
| ./server | Node.js/server createAuth factory |
| ./edge | Edge/Workers createAuth factory |
| ./backend | Backend helpers (session inspection, API keys) |
| ./cloud-admin-session | Better Auth plugin for Voyant Cloud broker callbacks |
| ./cloud-broker | Browser state and assertion exchange helpers for the Cloud broker |
| ./workspace | Current-user/profile helpers for mounted auth routes |
| ./permissions | Permission/role contracts |
License
Apache-2.0
