Self-hosting
Run IdRa on Railway or any container host with Postgres.
IdRa is one Bun process: the admin console, the end-user dashboard, hosted sign-in, the OIDC and SAML identity provider, SCIM, a job worker and a scheduler. It needs one Postgres database (Postgres 13+) and nothing else. Pending database migrations are applied at boot.
Docker
The repository's Dockerfile builds one image (API + built web app; it includes openssh-client for the SSH certificate authority).
docker build -t idra .
docker run -p 3000:3000 \
-e NODE_ENV=production \
-e DATABASE_URL=postgres://user:pass@db:5432/idra \
-e BASE_URL=https://idra.example.com \
-e TRUSTED_ORIGINS=https://idra.example.com \
-e BETTER_AUTH_SECRET="$(openssl rand -base64 48)" \
-e ENCRYPTION_KEY="$(openssl rand -base64 32)" \
-e TRUST_PROXY=forwarded \
idraPut a TLS-terminating proxy in front: IdRa must be served over https in production. The health check is GET /healthz (answers ok).
Railway
- New project → Deploy from GitHub repo. The repository's
railway.jsonselects theDockerfileand the/healthzhealth check. - Add a PostgreSQL service.
- Set the variables below (
DATABASE_URL=${{Postgres.DATABASE_URL}}). - Deploy, then create the first administrator (below).
Run one replica: caches and some rate limits are per process.
On SIGTERM IdRa stops taking requests, gives running jobs 8 seconds, puts unfinished ones back in the queue and exits within 10 seconds. Give your platform at least that long before SIGKILL (on Railway, the service's draining time or RAILWAY_DEPLOYMENT_DRAINING_SECONDS). A job cut off earlier is retried after 10 minutes.
Environment variables
| Variable | Default | Notes |
|---|---|---|
NODE_ENV | development | Set production. Production refuses to start with the dev secrets or a non-Postgres database. |
PORT | 3000 | Listening port. |
DATABASE_URL | file:.data/pglite | postgres://… in production. file:<dir> or memory:// run embedded PGlite (development only). |
BASE_URL | http://localhost:5173 | The public URL. Also the OIDC issuer origin (<BASE_URL>/api/auth): set it before the first sign-in. |
TRUSTED_ORIGINS | http://localhost:5173 | Comma-separated origins allowed to call the API with cookies. Same as BASE_URL in production. |
BETTER_AUTH_SECRET | dev value | Required. openssl rand -base64 48. |
ENCRYPTION_KEY | dev value | Required. openssl rand -base64 32. Encrypts every secret at rest (vault, SCIM tokens, signing keys). Back it up: losing it loses those secrets. |
TRUST_PROXY | none | railway (X-Real-IP, on Railway's edge), cloudflare (CF-Connecting-IP + geo headers) or forwarded (X-Forwarded-For, behind a proxy you run that overwrites it). Set it: without it rate limits can't see client IPs. Country/ASN signals and dynamic zones need cloudflare. |
RESEND_API_KEY | – | Email delivery through Resend. Without it, emails are written to the log. |
EMAIL_FROM | IdRa <noreply@localhost> | Sender address. |
COOKIE_DOMAIN | – | Parent domain for the session cookie, only for Access Gateway apps on subdomains (e.g. example.com). |
TENANT_BASE_DOMAIN | – | Turns on multi-tenancy. |
PLATFORM_TENANT | default | Operator tenant (multi-tenant). |
SIGNUP_ENABLED | false | Self-serve signup at /signup (multi-tenant). |
TENANT_POOL_MAX | 2 | Connections per tenant database (1–50). |
TENANT_POOL_IDLE_TIMEOUT | 30 | Seconds before an idle tenant connection closes. |
First administrator
bun run admin:create you@company.com "Your Name"On Railway, run it inside the service with railway ssh. It prints (or emails) an activation link: set a password, then enroll MFA at first sign-in; administrators always need MFA.
Flags: --platform creates a Platform Administrator in the operator tenant; --tenant <slug> creates the first admin inside a tenant.
Backups
Back up Postgres (Railway's Postgres backups, or pg_dump; with multi-tenancy also every idra_* database, or pg_dumpall). Keep ENCRYPTION_KEY and BETTER_AUTH_SECRET in a password manager: a database backup can't decrypt secrets without them.
Local development
bun install
bun run admin:create you@example.com "Your Name"
bun run dev # API on :3000, Vite on :5173 → http://localhost:5173Development uses embedded PGlite in .data/ (no Postgres needed). Stop the dev server before running scripts against .data/: PGlite allows one process at a time.