Authio docs

Concepts

Dedicated database tier

Authio runs every customer on a shared multi-tenant Postgres by default. Enterprise tenants can opt in to a fully isolated database — same code path, a different pool, with the per-store routing enforced at runtime.

Why a dedicated tier exists

Enterprise buyers regularly ask for “our data in our own database, not co-mingled with the rest of your customers.” WorkOS does not publish an equivalent tier. Authio ships one as part of the Enterprise plan: provisioning is operator-driven, the cutover is online, and the JWT shape your application reads doesn’t change.

Two tiers, one code path

  • Shared (default) — tenants live on a multi-tenant Postgres cluster behind the standard Authio pgbouncer. Row-level security pins every query to authio.tenant_id / authio.project_id.
  • Dedicated (Enterprise) — an isolated Postgres provisioned by Authio for the tenant. Same migration set, same RLS posture, same audit-log retention. Routed via a per-tenant pgxpool selected at the call site.

How routing works (T3.6 + T3.6.1, demo-ready)

Every store method now reaches for DB.PoolFromContext(ctx). The HTTP middleware stamps the resolved project_id (and, where available, tenant_id) into the request context using the public store.WithProjectID / store.WithTenantID helpers. The router resolves the ctx to one of:

  1. The shared pool — when the tenant is on db_strategy = 'shared'.
  2. The per-tenant pool — when the tenant is on db_strategy = 'dedicated'.
  3. The shared pool, fail-closed — when the tenant resolves to kind = 'platform'. Platform-tenant data never lands on a dedicated pool, even if a misconfigured row sets db_strategy = 'dedicated' alongside kind = 'platform'; the runtime returns ErrPlatformTenantNoDedicatedPool.

T3.6 (2026-05-21) shipped the router + provisioning binary + operator UI; the call-site refactor (T3.6.1, 2026-05-23) wired every internal/store/*.go method through DB.PoolFromContext. The Dedicated DB tier is now demo-ready: the next paying Enterprise tenant provisioning that flips db_strategy to 'dedicated' exercises the per-tenant pool end-to-end without any further code change.

Cross-tenant call sites that intentionally stay on shared

A small set of pre-resolution lookups must run on the shared pool because the project / tenant identifier is not yet known at query time. They are documented inline in authio_auth-core/internal/store/ and listed in the SHIP_REPORT_T3.6.1-2026-05-23.md report. The set is:

  • Custom-domain → project resolver (CustomDomainStore.GetActiveByHost).
  • Magic-link / OAuth state / email-challenge token consumers (the token is the only context).
  • Per-IP rate-limit counters that pre-date project resolution.
  • Allowed-origin / redirect-URI lookups (the very purpose is to tell us which tenant owns the inbound origin).
  • M2M client lookup at /v1/auth/token.
  • The signing_keys table — platform-level by architecture; rows pin to shared even for dedicated tenants.

What customers see

  • A “Request dedicated DB” toggle on the Enterprise billing surface in /billing. Operator-driven cutover today; self-serve provisioning is a follow-up.
  • No JWT shape change. The same access tokens that worked on shared continue to work on dedicated. Customer SDKs neither notice nor need to be re-versioned.
  • A dedicated operations runbook entry (runbooks/dedicated-db-operations.md) covering backup, restore, and the row-pinning matrix for the pre-resolution shared-pool queries listed above.
Dedicated DB pairs naturally with Bring Your Own KMS (Vault BYOK, P2). Both are sales-gated Enterprise upsells; the Dedicated DB tier is the single-tenant data plane that BYOK assumes.

Verifying invariants

Three runtime-level tests pin the platform-tenant invariant (T6.0) at the routing seam every store method reaches for:

  • TestRouter_PoolForOrErr_PlatformTenantReturnsSharedWithErr
  • TestRouter_PoolForOrErr_PlatformTenantWithDedicatedStrategyStillRefuses
  • TestDB_PoolFromContext_PlatformTenantEvenWithDedicatedStrategy

Audit-RLS / immutability (T4.5) is preserved end-to-end because every multi-statement transaction opens via PoolFromContext(ctx).Begin — single pool, single connection, the set_config('authio.project_id', ..., true) GUC binds correctly on whichever physical database the INSERT lands on.

Read next