Build and deploy
Every deployable unit in Surplus — the apps and the cron workers — ships as a Docker image built from the monorepo and runs on Railway.
Docker images
Each app defines its own Dockerfile, built with the repository root as the build context so it can access shared packages:
apps/client/Dockerfile— builds the static Vite bundle for the browser app.apps/server/Dockerfile— builds the Bun API server with the workspace packages it needs at runtime, and runs anenv-checkso a misconfigured deploy fails at build time rather than in production.apps/docs/Dockerfile— runs thefumadocs-mdxpostinstall andnext buildfor this docs site.crons/*/Dockerfile— one per scheduled worker.
Railway configuration
A railway.json sits next to each deployable unit and declares how Railway builds and runs it:
- the Dockerfile path and watch patterns (which file changes trigger a rebuild),
- replica counts and region hints,
- and, for cron workers, the
cronScheduleand arestartPolicyType: NEVERso jobs run to completion on schedule.
In production, the API server runs with three replicas and the web client with two. The cron schedules are listed in Cron workers.
The version label
The root package.json version field is the platform version. It's baked into builds as a VERSION value — the docs site, for example, shows it in the sidebar footer, and the client sends it as x-se-client-version on every API call so server logs can tie a request back to a specific client build.
Environments
Railway hosts the platform across environments (development, staging, production). Environment-specific behavior — CORS allowlists, cookie domains, whether Apitally is enabled — keys off the Railway environment name, which the apps read through their validated env. See Infrastructure and secrets for how secrets are managed across these environments.
Secrets
Secrets are never committed. Database connection strings, the JWT signing secret, S3 credentials, Stripe and Resend keys, and Mapbox tokens are injected by Railway and read only at the app layer (apps/server and apps/client build pipelines) — never inside reusable packages. The same injection mechanism powers local development through railway dev, which is why running the stack locally uses the Railway CLI.
Continuous integration
End-to-end tests run automatically in GitHub Actions (.github/workflows/e2e.yml) on pushes and pull requests targeting main and staging. CI uses the Railway CLI to inject configuration at runtime, so the only GitHub secret required is a scoped RAILWAY_TOKEN. See Testing for the details.