Multi-Tenancy
O2ID is multi-tenant: one instance hosts many isolated tenants, each its own identity universe — its own users, applications, roles, resource servers, and OIDC issuer. Tenants share nothing; a user in one tenant is invisible to every other.
Tenant-scoped URLs
Every endpoint is served under a tenant path prefix: /t/{tenant}/…, where
{tenant} is the tenant's slug. For example, the OpenID discovery document
for the acme tenant is at:
GET /t/acme/.well-known/openid-configuration
and it reports a per-tenant issuer:
{
"issuer": "https://id.example.com/t/acme",
"authorization_endpoint": "https://id.example.com/t/acme/oauth2/authorize",
"token_endpoint": "https://id.example.com/t/acme/oauth2/token"
}
The health check (/health) and the portal's static assets
(/portal/static/…) are the only global, un-prefixed paths.
The system tenant
On first run, o2id serve seeds a tenant named system and creates the
platform admin inside it.
The system tenant holds platform-level identities — the operators who manage
the deployment itself. o2idctl targets system by default.
Tenant-scoped identities
A user belongs to exactly one tenant. There is no cross-tenant membership: an
operator who administers several tenants has a separate account in each. Email
uniqueness is per-tenant, so alice@example.com can exist independently in two
different tenants.
Platform vs tenant administration
Two distinct levels of authority:
- Platform admin — holds the platform scope namespace (
tenants:*) and can create and manage tenants across the deployment. Seeded into thesystemtenant on first run. - Tenant admin — holds
*(every scope) within a single tenant. Can fully administer that tenant's users, apps, and roles, but has no cross-tenant authority.
Crucially, a tenant's * wildcard does not include the platform tenants:* scopes — so being a tenant admin never confers the ability to touch other tenants. Platform authority is granted explicitly, only by an existing platform
operator.
Tenant settings
Each tenant has its own settings — the pushed-authorization-request
request_uri TTL, the webhook SSRF allowlist for flow webhook nodes, and
which log levels get captured — read and updated with:
o2idctl tenant-settings get
o2idctl tenant-settings update --par-request-uri-lifetime 90s
A tenant's settings are its own defaults only — they have no effect on any other tenant. Token, session, and transaction lifetimes are configured per application instead — see Application settings.
Creating tenants
o2idctl tenants create --slug acme --name "Acme, Inc."
o2idctl tenants get <id>
o2idctl tenants list
Requires the tenants:create (or tenants:read for get/list) platform
scope. A newly created tenant gets its own OIDC signing key immediately —
every tenant, at any depth, has its own effective key, either its own or an
inherited one (see Organizations). Pass
--admin-email/--admin-password to seed a first admin (every scope,
within the new tenant only) in the same step:
o2idctl tenants create --slug acme --name "Acme, Inc." \
--admin-email admin@acme.example --admin-password Str0ngPassw0rd9
Organizations
A tenant can host its own customer accounts — Organizations — nested underneath it. See Organizations for creating, listing, and reasoning about them; from the outside an Organization behaves like a lightweight tenant of its own (own users, own roles, own optional signing key and settings), scoped entirely within its parent tenant.
Pass --admin-email/--admin-password to o2idctl tenants create to seed
a first admin alongside a new tenant; without them, a freshly created
tenant starts with no users, and its first user has to be created
separately via o2idctl users create once a role exists to assign them.
A freshly created Organization never seeds its own admin this way — see
Organizations — Delegated
administration for how it gets
its first user instead. Delegated Organization self-service (an
Organization's own admin managing its own members/roles without touching
its parent tenant's credentials) is not yet built — see Organizations'
current status for what's next.