Surplus logo
Surplus Docsby Sharing Excess

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 an env-check so a misconfigured deploy fails at build time rather than in production.
  • apps/docs/Dockerfile — runs the fumadocs-mdx postinstall and next build for 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 cronSchedule and a restartPolicyType: NEVER so 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.