Skip to content

Infrastructure as Code

All Railway configuration — services, sources, domains, replicas, volumes, buckets, variable keys — is managed as code with Railway IaC in the infra repo. The dashboard is for reading, not editing: manual edits are drift, and CI flags them daily.

Directory Railway project Environments
huddle-surety/ Huddle Surety — App, API, RAG, Notification, their databases and bucket production, staging, development
infrastructure/ Infrastructure — Temporal (server, UI, Postgres) and the observability stack production

Each directory holds one .railway/railway.ts describing the whole project. One file covers every environment — differences (branch, domains, replica counts, the production-only legacy o11y stack) are expressed with the environment context:

const prod = ctx.isEnvironment("production");
const branch = prod ? "production" : "staging";
const host = (sub: string) =>
prod ? `${sub}.huddlesurety.co` : `${sub}.${ctx.environmentName}.huddlesurety.co`;

Change infrastructure by editing railway.ts and opening a PR:

  1. PR — CI runs railway config plan for every project × environment and shows the exact changes in the log.
  2. Merge to main — CI runs railway config apply --yes, applying the plan to Railway.
  3. Daily cron — CI re-plans with --detailed-exit-code and fails on drift, catching dashboard edits that bypassed the repo.

Destructive changes (deleting a service, variable, or volume) are never auto-applied: CI deliberately omits --confirm-destructive, so such a plan fails the workflow and must be applied manually from a terminal.

Locally:

Terminal window
cd infra && bun install
cd huddle-surety # or infrastructure
railway environment staging
railway config plan # needs node_modules/.bin on PATH (railway-iac-ts)
  • Owns service settings: source repo/branch, builder, start command, healthcheck, replicas per region, custom domains, TCP proxies, private network endpoints, volumes and mounts, memory limits, canvas groups.
  • Owns non-secret wiring variables — environment-derived values (DEPLOYMENT_ENVIRONMENT, the app.…/api.….huddlesurety.co URLs) are literals computed from the environment context, and cross-service URLs are Railway reference templates (http://${{API.RAILWAY_PRIVATE_DOMAIN}}:8080).
  • Keeps secrets as preserve() — “keep what’s set in Railway”. IaC guards the key set (removing a key from the file deletes the variable — a destructive change), while secret values stay in Railway.
  • Doesn’t replace rag/railpack.json — runtime apt packages (poppler-utils, tesseract-ocr) and the pre-install GITHUB_TOKEN git rewrite for the private SDK have no IaC equivalent. Build plans stay with the builder; service settings live in IaC. rag/railway.toml was removed — a service must not be managed by config-as-code and IaC at once.
  • References into database-class resources (${{API Postgres.PGHOST}}, ParadeDB’s DATABASE_URL) are redacted by Railway on read-back, so asserting them in railway.ts re-diffs — and redeploys — on every plan. They are set in Railway but kept preserve() in the file.
  • CI needs the RAILWAY_API_TOKEN repo secret (a Huddle workspace token).