← Back

Capabilities

A schema per tenant, not a tenant column

Isolation enforced by PostgreSQL rather than by remembering a WHERE clause. Each tenant is its own schema, its own issuer, its own key set and its own branded sign-in.

Most products that call themselves multi-tenant put a tenant identifier in a column and add it to every query. It works until the day somebody writes a query without it. That day arrives through a reporting job, a migration script, a support tool, or a nested subquery where the condition was applied to the outer statement and not the inner one. The failure mode is the worst kind: correct-looking code that returns somebody else's data.

SkipAuth puts each tenant in its own PostgreSQL schema, tenant_<name>, and resolves the tenant per request rather than per query.

How it works

A request carries its tenant, resolved in a fixed order of precedence: an explicit tenant header, then a tenant name header, then a query parameter, then the tenant claim in the token. That resolution is stored in request-scoped context, and a thin database proxy issues a SET LOCAL search_path for each transaction before any statement in it runs.

The consequence is that application code does not carry the tenant at all. There is no condition to forget, because there is no condition. A query for identities reaches the identities of exactly one tenant, decided before the statement was parsed.

Three things follow from that:

  • Blast radius. A defect in a query is a defect inside one tenant.
  • Data residency is a deployment decision. Because you run the database, where a tenant lives is where you put the cluster, not a region setting in a vendor's console.
  • Per-tenant identity is cheap. Once a tenant is a schema, giving it its own OIDC issuer, its own SAML metadata, its own signing keys and its own branded sign-in page is bookkeeping rather than architecture.

Cross-tenant material is deliberately elsewhere: the tenant registry, signing keys, system configuration and the application-sharing graph live in the shared schema, because they are the things that must be true across tenants.

How to check it

-- as the application role, inside one request context
SELECT current_setting('search_path');

Then the test worth running: create two tenants, put a person in each, and try to read across. Not through the API, which is doing its job, but by taking an identity handler and calling it under one tenant context with an identifier belonging to the other. It returns nothing, because it is looking in a schema where that row does not exist.

Then look at the migration count. There are more than seventy migrations, and they are idempotent and applied per schema. This is the operational cost of the approach and it is worth seeing before you adopt it.

Where it stops

Migrations multiply. A schema change runs once per tenant. At a handful of tenants this is invisible. At a thousand it is a scheduled operation you plan, and anybody telling you otherwise has not run it.

Cross-tenant analytics get harder. Counting identities across every tenant means visiting every schema. This is the direct trade for the isolation, and it is the right trade for identity data and the wrong trade for, say, an events warehouse.

A tenant is not free. Each one carries its own tables, indexes and connection footprint. Schema-per-tenant suits tens to thousands of tenants. It is not the design you would choose for millions of consumer accounts, and SkipAuth is not a consumer identity platform.

What the alternatives ask of you

Approach What it asks of you SkipAuth
A tenant column with row-level filtering Discipline in every query, forever, including in the tools nobody reviews. The tenant is set on the connection before the statement runs, so there is no condition to omit.
Realms in a self-hosted provider Accept configuration-level separation that shares the same tables underneath. Genuine schema separation, enforced by the database.
One deployment per tenant Operate the same stack many times, with the cost multiplied by the tenant count. One process, many schemas.
A hosted product with a tenant setting Trust the isolation and the region label, and take the vendor's word for both. You run it, so you can read the search path yourself.

Read next: roles as a graph, with scopes that combine.