Multi-tenancy
Host many independent organizations on one deployment, each with its own database.
One IdRa deployment can host many organizations (tenants). Each tenant has its own Postgres database on the same server, so a tenant's queries can only reach its own data. Each tenant gets its own address, OIDC issuer, signing keys, settings, users, apps, policies, System Log and jobs.
Tenancy is off by default: without TENANT_BASE_DOMAIN, IdRa is single-tenant.
Turn it on
Set TENANT_BASE_DOMAIN, for example idra.example. Then:
- Each tenant lives at
https://<slug>.idra.example, or at a verified custom domain. - The existing database becomes the default tenant, still at
BASE_URL. Nothing is copied or rewritten. - Unknown hosts get 404; suspended tenants get 403.
X-Forwarded-Hostis never trusted.
On Railway, add the wildcard *.idra.example as a custom domain on the service (plus the operator host from BASE_URL), and create the DNS records Railway shows.
What's per tenant
- OIDC issuer
https://<tenant host>/api/auth, its JWKS, SAML certificates, SCIM tokens, admin API tokens, Shared Signals streams and Fleet webhook secrets. - Branding, email templates, policies, System Log.
- Cookies are host-only. A tenant on a custom domain can widen them for Access Gateway apps (Customizations → Domains → Access Gateway cookie domain).
- Session and token secrets are derived from
BETTER_AUTH_SECRETplus the tenant id;ENCRYPTION_KEYis shared. - The same email address can exist in two tenants as two unrelated people, with separate passwords and sessions.
Platform administrators
bun run admin:create you@company.com "Your Name" --platformThis creates a Platform Administrator in the operator tenant (PLATFORM_TENANT, default default). The platform area is at https://<operator host>/platform (also in the admin console's user menu) and only answers on that host. There you can:
- create tenants (database, migrations and the first admin's activation email), and follow new organizations under Onboarding;
- suspend and resume them (a suspended tenant's jobs wait);
- delete them (type the slug to confirm; the database is dropped);
- manage domains and limits (users, apps, API requests per minute: authenticated API and OAuth-client requests; sign-in and console traffic don't count), and see platform events.
The platform_admin role can't be granted from the console or the admin API. To create the first admin inside an existing tenant: bun run admin:create admin@customer.com "Name" --tenant <slug>.
Self-serve signup
Set SIGNUP_ENABLED=true to open https://<operator host>/signup. A new organization goes through these steps:
- Sign up. The person enters the organization name, an address (checked live: invalid, reserved and taken addresses are refused as they type), their name and work email. Nothing is created yet: the address is held for 24 hours and a confirmation link is emailed.
- Confirm the email. The link opens a confirmation page on the operator host. Selecting Create organization proves the address and starts provisioning. Opening the link alone does nothing, so mail scanners can't create organizations.
- Provisioning. IdRa creates the database, applies migrations, seeds defaults and the organization name, and creates the first Super Administrator, then sends the browser to
https://<slug>.<base domain>/login. - Activate. The admin gets an activation email (printed in the log without
RESEND_API_KEY), sets a password, signs in and enrolls MFA. - Setup wizard. The console opens
/admin/setup: secure the account, branding, invite people, add a first app. Any step can be skipped; Finish later leaves a link on the Overview page, and the wizard resumes where it was.
Asking again with the same email replaces the link. If provisioning fails, the database and registration are removed, the person sees a Try again button on the same link, and the operator sees the reason. Only tenants created after this feature shipped get the wizard.
Abuse protection
- Each IP can attempt 5 signups per hour, and each email address 3. The availability check allows 60 lookups per IP per minute. Set
TRUST_PROXY, or every visitor shares one bucket. - The operator tenant's CAPTCHA (Security → General) is required when one is configured.
- No database exists until an email address is confirmed, and an unconfirmed address is released after 24 hours.
Tracking onboarding
Platform → Onboarding lists every signup, and every tenant an operator created with a first admin:
| Status | Meaning | Actions |
|---|---|---|
| Awaiting email verification | The link hasn't been confirmed | Resend verification, Cancel |
| Link expired | Not confirmed within 24 hours. The address is free | Cancel |
| Provisioning | Running now | |
| Failed | Rolled back; the reason is shown | Retry, Cancel |
| Awaiting activation | The tenant is live; the admin has no password yet | Resend activation |
| Setting up | The admin activated; the wizard isn't finished | |
| Complete | The wizard is finished |
Cancelling kills the link and frees the address. A tenant that already exists is removed with Delete on its page. Each step is a platform event (platform.signup.*, platform.tenant.create, platform.onboarding.*), shown under Events; the wizard's steps are in the tenant's own System Log (system.setup.*). Abandoned signups are removed at boot a week after they expire.
Custom domains
- A tenant admin adds the hostname under Customizations → Domains.
- They create the TXT record shown (
_idra.<host>=idra-verification=<token>) and a CNAME from the host to the service, then select Verify. - The operator adds the hostname to the Railway service for TLS (or fronts it with Cloudflare for SaaS).
- Make primary moves the issuer and sign-in URLs to that domain.
Changing the primary domain
OIDC and SAML apps need the new issuer, sessions on the old host don't carry over, and passkeys registered on the old host stop working.
Database and connections
- The
DATABASE_URLrole needsCREATEDB(Railway'spostgresuser has it). Tenant databases are namedidra_<id>; deleting a tenant runsDROP DATABASE … WITH (FORCE). - The main pool (10 connections) plus active tenants ×
TENANT_POOL_MAX(default 2) must stay below Postgresmax_connections. Idle tenants close their connections afterTENANT_POOL_IDLE_TIMEOUTseconds. - Still one replica: the tenant cache and the signup rate limits are per process.
Development
TENANT_BASE_DOMAIN=localhost, then open http://<slug>.localhost:5173. Browsers resolve *.localhost to loopback. Tenant databases live under .data/pglite-tenants/.
Migrate from Okta
What maps from Okta to IdRa and what doesn't, exporting users, groups and apps, issuer changes, cutover and rollback.
Okta-compatible Admin API
Automate IdRa through its Okta-compatible /api/v1 management API: users, groups, System Log, applications, policies and event hooks. Okta SDKs and scripts work against it.