Railway Platform Build Configuration

Version: 1.2
Date: April 2026
Owner: AI Team


1. Purpose

Optimize Railway build times and PR preview environments for the brainforge-platform monorepo by:

  1. rootDirectory — Build only the app subtree (e.g. apps/platform) instead of the full monorepo (smaller context, faster upload)
  2. Railpack — Use Railpack (default) as the builder for Node services where applicable (better caching)
  3. legacy-peer-deps — Resolve @smithy peer dependency conflicts via .npmrc
  4. watchPatterns (config as code + dashboard) — Gitignore-style paths from the repository root (Railway often stores them with a leading /, e.g. /apps/platform/**) so Focused PR environments deploy only services touched by the PR diff
  5. PR template hygiene — Keep the environment PRs clone from as small as practical (see §7)

2. Config Files (in repo)

FilePurpose
apps/platform/.npmrcSets legacy-peer-deps=true so Railpack’s auto-detected npm ci resolves peer deps
apps/platform/nixpacks.tomlNixpacks fallback config (only used if builder switches from Railpack)
apps/platform/railway.tomlbuild.watchPatterns + RAILPACK for platform web
apps/slack-apps/brainforge-assistant/railway.tomlwatchPatterns for the Slack assistant service
apps/opencode-worker/railway.tomlDockerfile build + watchPatterns for the worker
apps/marketing-site/railway.tomlwatchPatterns only (builder follows dashboard / Railpack + nixpacks.toml)

Do NOT add a railpack.json — Railpack auto-detects Node/npm from package.json and package-lock.json. Custom step overrides in railpack.json can break the BuildKit dependency graph, causing source files to be unavailable during install.

Config as code precedence: Values in railway.toml / railway.json override the dashboard for that deployment. See Config as Code.

If a service does not pick up railway.toml: In Railway Service → Settings, set the config file path to the repo-root path (e.g. apps/platform/railway.toml). The monorepo note that the config file does not follow Root Directory applies to discovery in some setups—explicit path fixes it.


3. Railway Service Config (one-time)

Run these commands after linking to the project. Replace <service> with the Railway service name (see railway service list).

# 1. Link to project (if not already)
railway link --project brainforge-platform
 
# 2. Platform web: root directory + Railpack (watch patterns also in apps/platform/railway.toml)
railway environment edit --service-config <platform-service> source.rootDirectory "apps/platform"
railway environment edit --service-config <platform-service> build.builder RAILPACK
 
# 3. Optional CLI mirror of watch patterns (usually redundant if railway.toml is loaded)
railway environment edit --service-config <platform-service> build.watchPatterns '["/apps/platform/**"]'

Non-interactive batch update (recommended in CI / headless shells): railway environment edit --service-config can block waiting for a TTY. Pipe a single JSON patch on stdin instead (resolve service UUIDs from railway status --json first; keys must be IDs, not names):

railway link --project brainforge-platform --environment production
railway environment edit -m "chore: monorepo watch patterns for focused PR" --json <<'EOF'
{
  "services": {
    "<web-service-uuid>": { "build": { "watchPatterns": ["/apps/platform/**"] } }
  }
}
EOF

Other services in the same project should set source.rootDirectory to their app folder (e.g. apps/slack-apps/brainforge-assistant, apps/opencode-worker) and use the matching build.watchPatterns JSON array, or rely on the committed railway.toml in that folder.

Watch pattern reference (paths from repo root, leading slash matches production):

AppPattern
Platform (web)/apps/platform/**
Slack assistant/apps/slack-apps/brainforge-assistant/**
OpenCode worker/apps/opencode-worker/**
Marketing site/apps/marketing-site/**
Files proxy/apps/files-proxy/**
HubSpot lead nudge/apps/hubspot-lead-nudge/**
Paperclip/apps/paperclip/**

Add patterns for any shared paths that must trigger a rebuild (e.g. a future packages/shared/**). If a PR changes only knowledge/ or unrelated apps, Focused PR should skip services whose patterns do not match. Postgres and other plugins typically have no repo watch paths—leave them unset.


4. Why .npmrc (not railpack.json or env vars)

  • railpack.json custom step overrides break the BuildKit layer dependency chain, causing ENOENT errors during npm install
  • RAILPACK_INSTALL_CMD env var works but must be set per-environment; .npmrc is committed and consistent everywhere
  • .npmrc with legacy-peer-deps=true lets Railpack use its default npm ci while resolving the @smithy peer dep conflicts

5. Before pushing (catch Railway failures early)

Run this before pushing platform changes to avoid Railway build failures:

cd apps/platform && npm run build:verify

This runs the build with SLACK_SUPABASE_URL and SLACK_SUPABASE_SERVICE_KEY unset, simulating Railway PR environments. If it passes locally, Railway will pass.


6. PR preview environments (Railway UI)

  1. Project → Settings → Environments: enable PR environments and Focused PR environments (docs).
  2. Watch paths / watchPatterns: Focused PR uses changed files vs watch paths or service root directory—committed railway.toml keeps this in git.
  3. Dependencies: Services referenced via ${{otherService.*}} are pulled in when the affected service deploys; trim unnecessary references in PR templates if previews do not need them.
  4. Domains: PR previews need a Railway-generated domain on the base service for automatic preview URLs (docs).

7. Slim the environment PRs clone from

PR environments copy from a base environment (confirm which one in Project → Settings → Environments).

GoalAction
Fewer cold startsRemove services from the base that previews never need (extra workers, internal tools).
Less provisioningPrefer one database (or none) for previews if the app allows it; avoid cloning full production topologies.
Clearer graphPut rarely-previewed apps in a separate Railway project (e.g. marketing already uses its own project + workflow promotion).
Staging as templateMaintain a staging (or preview-base) env that mirrors production only for “web + deps reviewers need,” and use it as the PR source if Railway allows selecting it.

8. Triage a slow deployment (build vs healthcheck vs runtime)

Use the deployment timeline in the Railway UI (per deployment, not only the service card). Typical phases: build → image/push → deploy/starthealth check.

Phase dominatesLikely causeLevers
Buildnpm ci, Docker layers, cold cacherootDirectory, .dockerignore, fewer deps, watchPatterns, Railpack cache
Deploy / startMigrations, heavy bootFaster readiness path, defer non-critical init
Health checkApp not listening yet; wrong pathCorrect healthcheckPath, fix slow startup; adjust timeout only if truly needed (slow deployments)

CLI:

railway logs --build --environment <env> --lines 300
railway logs --environment <env> --lines 200

If build logs finish quickly but the deployment stays “in progress,” investigate runtime and healthcheck, not install.