Architecture
Two apps, one config package
apps/auth(c-auth.mvscloud.com) — tenant user authentication, the/account/*self-service surface (profile, 2FA, passkeys, backup codes, sessions), the registry-driven app switcher, and the OIDC Identity Provider.apps/platform-auth— operator authentication for MVS internal staff, hosted atc2-auth.mvscloud.com. Okta (OIDC + SAML fallback) is the primary operator sign-in; SCIM provisioning is wired from Okta.
Platform-AUTH (c2-auth.mvscloud.com) and platform-ADMIN
(c2.mvscloud.com) are distinct hosts, sharing the .mvscloud.com cookie
so operators authenticate once and carry the session to admin. The passkey
relying-party ID is mvscloud.com.
Both apps mount Better Auth instances defined in
packages/auth-server/central-auth.ts and
packages/auth-server/platform-auth.ts (Better Auth 1.7.0-beta.5, pinned
exact). packages/auth-client is the React/HTTP client downstream apps use.
Database isolation
All Better Auth tables live in a dedicated Neon project (mvs-auth-db)
reached via AUTH_DATABASE_URL through the auth-only Prisma client
@mvs/auth-db. Auth state is never co-resident with control-plane business
data or tenant data. A Vercel cron (/api/cron/keep-warm, every 4 minutes)
defeats Neon autosuspend.
Both auth apps — and both OIDC providers — share this single database. There is no second database for the platform surface; isolation is purely by table namespacing, keyed to two distinct user directories:
The two surfaces never share rows. Central OIDC clients and signing keys live
in oauth_client / jwks and FK to User; platform OIDC clients and keys
live in platform_oauth_* / platform_jwks and FK to PlatformUser.
Cross-surface use of an Initial Access Token or client is rejected.
Sessions and cookies
- Sessions share automatically across
*.mvscloud.com(cookie-scoped,crossSubDomainCookies), with JWE-encrypted cookie caching. - The hop to a different eTLD (
mvspay.io) uses the legacy cross-domain token flow (/api/cross-domain-token→/api/exchange-token): one-time, SHA-256-hashed, 60-second tokens. - The OIDC Identity Provider is the long-term replacement for that bespoke flow.
OIDC Identity Providers (two surfaces)
The estate runs two independent OIDC providers, one per auth app. They share
the same engine and the same mvs-auth-db, but are isolated by table namespacing
and back distinct user directories (see Database isolation):
Both providers are live in production. Both sign ID tokens with EdDSA
(Ed25519) using their own keys, require PKCE (S256 only), and set
allowDynamicClientRegistration: false — the RFC 7591 /oauth2/register
endpoint is off, so there is no dynamic client registration on either surface.
Each provider’s gate is read at cold start; when off, that surface’s
discovery document returns 404.
Central provider — c-auth.mvscloud.com
Gated by OIDC_PROVIDER_ENABLED=true (on in production):
- Discovery:
/.well-known/openid-configuration - JWKS:
/api/auth/jwks(EdDSA / Ed25519) - Authorization-code flow with PKCE required (S256 only) at
/api/auth/oauth2/authorizeand/api/auth/oauth2/token - Opaque access tokens (
mvs_at_*) validated via/api/auth/oauth2/introspect - Namespaced custom claims (
https://mvscloud.com/*:org_id,org_slug,role,tenant_db) — sourced from tenant-org membership — in the ID token and userinfo
Relying parties are registered as oauth_client database rows one of two
supported ways: the gated, authenticated self-service registration API
(POST /api/auth/admin/oauth-clients, admin session or scoped Initial Access
Token) or the operator CLI pnpm --filter @mvs/auth-server oidc:register. The
OIDC_CLIENT_* env triplets are vestigial.
Platform provider — c2-auth.mvscloud.com
The operator-side equivalent of the central IdP: a full OIDC provider for MVS
internal apps, gated by PLATFORM_OIDC_PROVIDER_ENABLED=true. Its issuer is
pinned via PLATFORM_OIDC_PROVIDER_ISSUER_URL (default
https://c2-auth.mvscloud.com):
- Backs the
PlatformUserdirectory and writes theplatform_oauth_*/platform_jwkstables — neveroauth_client/jwks - Namespaced custom claims (
https://mvscloud.com/*:role,groups) sourced fromPlatformUser, not from tenant-org membership - Operator relying parties register out-of-band as
platform_oauth_clientrows via the platform admin / Initial Access Token registration API (surface=platform); platform IATs and clients are rejected on the central surface and vice versa
See the operator-app OIDC integration guide to wire an operator app as a relying party.