# Railway Staging & PR Preview Environments ClassroomIO's production SaaS runs on Render + Cloudflare — **not** Railway. This setup adds a **separate, internal Railway project** that provides a long-lived **staging** environment plus **short-lived preview environments** for testing a branch/PR of the full stack (api, dashboard, jobs, Postgres, Redis, MinIO) before it merges to `main`. Real users are never routed here; it's a team-only sandbox. ## How to deploy to staging Push or merge your change to the branch connected to the staging environment (staging). Railway picks it up automatically and redeploys — no manual step required. ## How to deploy a PR to a preview environment Two mechanisms, both enabled: | | Native PR environments | Manual workflow (`railway-preview.yml`) | |---|---|---| | Trigger | Automatic on every opened PR | You run it from the **Actions** tab | | Teardown | Automatic on PR merge/close | `action = destroy`, or auto via `railway-preview-teardown.yml` on PR close | | Control | Whole base env is duplicated | Same, but you choose when and can re-seed the DB | | Requires | App services connected to the GitHub repo (Dockerfile builder) | A Railway account/team token in CI | > A service that deploys from a **fixed Docker Hub image tag can never get a PR preview** — > an image tag has no branch. That's why the Railway app services build **from the repo** > via `docker/Dockerfile.*`, not from `docker-compose.images.yaml`. ### Using the manual workflow > 🎥 Demo video: TODO **Actions → "Railway Preview Environment" → Run workflow:** - `branch` — the branch/PR head to deploy. - `action` — `deploy` (create/update) or `destroy` (delete). - `seed` — also load the full demo dataset (`admin@test.com` etc.); migrations + essential seed always run on api boot regardless. The run prints the preview dashboard URL in its job **summary**. The env is named `pr-`. The deploy step runs `railway up --ci` (no `--detach`), which streams build logs and **blocks until each service passes its deploy/healthcheck** before the next one starts — so the printed URL is live the moment the run finishes, and a failed healthcheck fails the run rather than reporting a broken preview. ## Database behavior - Each environment has its **own isolated Postgres** — previews start fresh. - `cio-api` **self-migrates on boot** (`docker/entrypoint-api.sh` → `pnpm --filter @cio/db db:setup` → `drizzle-kit migrate` + essential seed). No manual migrate step needed. - **The branch must include a committed Drizzle migration** (`pnpm --filter @cio/db db:generate`) — the boot path runs committed migrations only, not schema `push`. ## Verify before relying on it (spike, do on a throwaway env) Railway's docs are thin on a couple of points the workflow depends on — confirm these once: 1. **`railway environment new --duplicate ` copies service config *and* variables.** If your CLI version lacks `--duplicate`, use the GraphQL `environmentCreate(sourceEnvironmentId)` mutation instead. 2. **A duplicated env's Postgres starts empty** (so boot migration builds a clean schema). 3. **The demo-seed step's DB reachability.** `railway run` executes on the GitHub runner, so it needs a DB URL reachable from CI (Railway's public TCP proxy `DATABASE_PUBLIC_URL`), not the private `railway.internal` one. If unreachable, drop the seed step and rely on the boot-time essential seed. These caveats are marked `# SPIKE:` in `railway-preview.yml`. ## Cost Every environment (staging + each preview) runs the full 6 services incl. a MinIO volume, so previews are not free while alive. Destroy them promptly — the teardown workflow handles PR close automatically; use `action = destroy` for previews whose PR is still open.