IdRa

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 \
  idra

Put a TLS-terminating proxy in front: IdRa must be served over https in production. The health check is GET /healthz (answers ok).

Railway

  1. New project → Deploy from GitHub repo. The repository's railway.json selects the Dockerfile and the /healthz health check.
  2. Add a PostgreSQL service.
  3. Set the variables below (DATABASE_URL = ${{Postgres.DATABASE_URL}}).
  4. 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

VariableDefaultNotes
NODE_ENVdevelopmentSet production. Production refuses to start with the dev secrets or a non-Postgres database.
PORT3000Listening port.
DATABASE_URLfile:.data/pglitepostgres://… in production. file:<dir> or memory:// run embedded PGlite (development only).
BASE_URLhttp://localhost:5173The public URL. Also the OIDC issuer origin (<BASE_URL>/api/auth): set it before the first sign-in.
TRUSTED_ORIGINShttp://localhost:5173Comma-separated origins allowed to call the API with cookies. Same as BASE_URL in production.
BETTER_AUTH_SECRETdev valueRequired. openssl rand -base64 48.
ENCRYPTION_KEYdev valueRequired. openssl rand -base64 32. Encrypts every secret at rest (vault, SCIM tokens, signing keys). Back it up: losing it loses those secrets.
TRUST_PROXYnonerailway (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_FROMIdRa <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_TENANTdefaultOperator tenant (multi-tenant).
SIGNUP_ENABLEDfalseSelf-serve signup at /signup (multi-tenant).
TENANT_POOL_MAX2Connections per tenant database (1–50).
TENANT_POOL_IDLE_TIMEOUT30Seconds 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:5173

Development uses embedded PGlite in .data/ (no Postgres needed). Stop the dev server before running scripts against .data/: PGlite allows one process at a time.

On this page