Multi-tenancy
Multi-tenancy here is not a column in a table. A tenant is addressed by host, and every tenant carries its own cryptographic identity.
https://acme.oauth.work/.well-known/openid-configuration # Acme's issuerhttps://globex.oauth.work/.well-known/jwks.json # a different key entirelyhttps://oauth.work/.well-known/openid-configuration # the platform's ownWhat each tenant gets
Section titled “What each tenant gets”- Its own Ed25519 signing key, generated on first use and envelope-encrypted at rest.
- Its own OIDC discovery document and JWKS, served from its own host.
- Its own
did:web:<slug>.oauth.workissuer for verifiable credentials. - Its own audit trail, webhooks, log streams, roles, and members.
A flow run on acme.oauth.work issues tokens with iss: https://acme.oauth.work, signed by Acme’s
key. Each tenant is a self-contained OpenID Provider, not a view over a shared one.
Resolution is by Host header
Section titled “Resolution is by Host header”The tenant is derived from the request’s Host header — not a path segment, not a query parameter,
not a claim in a token. acme.oauth.work resolves to Acme; the apex resolves to the platform.
Discovery, JWKS, and DID documents therefore differ by host, and code that verifies tokens must
follow iss rather than a hardcoded URL:
// Right: resolve the key set from the token's own issuerconst { iss } = decodeJwt(token)const jwks = createRemoteJWKSet(new URL(`${iss}/.well-known/jwks.json`))await jwtVerify(token, jwks, { issuer: iss, audience: clientId })Reserved subdomains — www, api, admin, console, auth, docs, status, app, and others
the platform serves itself — cannot be claimed as tenant slugs. That is a security boundary, not
tidiness: a tenant on docs.oauth.work could serve a branded login page on a hostname users have
reason to trust.
Creating a tenant
Section titled “Creating a tenant”POST /v1/organizationsAuthorization: Bearer sk_live_…
{ "name": "Acme Corp", "slug": "acme", "domain": "acme.com" }Only a platform-scoped key may create organizations. An org-bound key gets 403 forbidden with
platform-admin key required — creating a tenant is not something one tenant’s key can do.
For self-serve signup — email code, org, admin user, and first API key in one flow — use the CLI as shown in the quickstart.
Read and update an org with GET /v1/organizations/:id and PATCH /v1/organizations/:orgId.
Branding fields on the org (display name, logo, primary colour) drive the hosted sign-in screens, so
a customer’s users see their own name at login rather than yours.
Verified domains
Section titled “Verified domains”A domain proves that an organization controls an email namespace. A verified domain is what lets
/authorize route a user to their own employer’s SSO connection: when the
request carries a login_hint with an email address and no org is otherwise bound, the domain
resolves the org, and that org’s active connection brokers the login.
Add the domain:
POST /v1/organizations/org_01J…/domainsAuthorization: Bearer sk_live_…
{ "domain": "acme.com" }{ "domain": "acme.com", "status": "pending", "txt_record": { "name": "_oauth-work-challenge.acme.com", "type": "TXT", "value": "oauth-work-verification=8f3b2c…" }}Publish that TXT record, then verify:
POST /v1/organizations/org_01J…/domains/acme.com/verify{ "domain": "acme.com", "status": "verified" }A failure returns 422 verification_failed with the reason — usually the record has not propagated
yet. A domain may be verified by at most one organization; a second org attempting the same
domain is refused even with a valid record, so an email namespace cannot be claimed twice.
Re-adding a domain resets its challenge to a fresh token and returns it to pending.
Your customer’s IT admin can do all of this themselves through an
Admin Portal link with intent: "domains" — which is usually better, since they
are the ones with access to the DNS zone.
What isolation guarantees
Section titled “What isolation guarantees”- Cryptographic — a tenant’s tokens and credentials are signed by that tenant’s key. Verifying Acme’s token against Globex’s JWKS fails, regardless of any application-level mistake.
- Query-level — every org-scoped route checks the caller’s org binding alongside its permission,
in one guard. An org-bound API key addressing another org gets
403 forbidden; a resource id from another org comes back404. - Operational — audit rows, webhook endpoints, log streams, and connected accounts are all org-scoped, and org-scoped keys are clamped to their own org even when they ask for another.
Exporting a tenant
Section titled “Exporting a tenant”GET /v1/organizations/org_01J…/exportAuthorization: Bearer sk_live_…Tenant signing keys stay platform-managed and are not exported. Everything the standards make portable — OIDC, SAML, SCIM, VC-JWT — remains portable by design: a credential you issued verifies against a published DID document whether or not you are still a customer.