Deployment
How apps ship to staging and production on Railway via GitHub Actions.
Deployment is fully automated. You commit, open a PR, and your app lands on staging with a preview URL. Merge to main, and it lands on production. Here's what's actually happening under the hood so you can debug when something goes sideways.
Architecture
- 1 Railway Project — the platform boundary (auth, networking).
- N Railway Services — one per app in
apps/*. Each service deploys independently. - Railpack builds the app (zero-config); it reads the root
pnpm-workspace.yamland builds only your app plus its dependencies.
Per-App Wiring
Each app needs three things:
apps/<app-name>/deploy.config.yml— points at the Railway service and declares env-var mappings..github/workflows/deploy-app-<app-name>-staging.yml— a manualworkflow_dispatchworkflow (used for one-off staging deploys; not what runs on a PR — see next section)..github/workflows/deploy-app-<app-name>-production.yml— triggers on push tomainwith a path filter. Most apps filter toapps/<app-name>/**+shared/database/**; a few apps that depend more broadly on shared code (e.g.vibe-coding-guide,staff-portal) filtershared/**instead. Check your app's workflow file to see which.
Both app workflows call the shared .github/workflows/deploy-reusable.yml, which does the actual work: pnpm install --frozen-lockfile → optional test → pnpm --filter <app> build → railway up.
The Two Paths a Deploy Can Take
Staging (when you open a PR)
A single workflow, pr-preview-neon.yml, orchestrates the whole preview:
- You push to your feature branch and open a PR to
main. - If the schema changed, a migration is generated and committed back to the PR.
- One Neon DB branch is created for the PR (
preview/pr-<number>-<branch>), 14-day expiry, with migrations applied. pr-preview-neon.ymldiscovers which apps changed and, in a matrix job, callsdeploy-reusable.ymldirectly for each one — it does not dispatch the per-app staging workflows.- Each changed app deploys to Railway's staging environment, connected to the shared Neon PR branch.
- A PR comment is posted with a preview URL per affected app.
The Neon PR branch is deleted when the PR closes.
Production (when you merge)
- You merge the PR to
main. - The production workflow for each affected app triggers via the path filter.
- Migrations apply to Neon main.
- Railway redeploys the production service.
Why You Don't See Secrets in Code
Railway environment variables are injected by the reusable workflow at deploy time. The workflow reads apps/<your-app>/deploy.config.yml's secrets: block, pulls values from GitHub secrets/vars, and pushes them to the Railway service environment. See Environment Variables for the full mapping flow.
Logs
Railway captures stdout and stderr only. Use console.log, console.warn, console.error. In production, log JSON so you can search on fields:
console.error(JSON.stringify({
level: 'error',
event: 'api_error',
error: error instanceof Error ? error.message : 'Unknown',
timestamp: new Date().toISOString(),
}));Never log PII (emails, names) or secrets. Never write logs to files or the database.
Auto-injected Variables
You don't need to declare these — they're set by the platform:
DATABASE_URL— injected from the environment's database service.AUTH_UI_URL— injected for every non-auth service.
Ask Claude
When a Deploy Fails
- Check the workflow run in GitHub Actions to see which step failed.
- Lint/typecheck fail → run
pnpm build:<your-app>locally to reproduce. railway upfails → open the Railway dashboard; service logs will usually tell you why.- Missing env var → see Environment Variables.
- Migration fail → see Database Migration Errors.
Quiz
You merge a PR to main that only changes files under apps/meal-planner/. Which production services redeploy?