This page covers the frontend side of shipping: what the build produces and how the repo’s workflows package it. Server access details and secret values are not documented here on purpose.

The standalone build

apps/saas/next.config.ts:
  • output: "standalone" makes next build emit apps/saas/.next/standalone/, a self contained tree with apps/saas/server.js, the traced workspace packages and a slim node_modules.
  • outputFileTracingRoot points at the monorepo root so files from packages/* are traced in.
  • deploymentId ties the client bundle to a release, which lets Next.js detect a browser running an older build.
Standalone output does not include static assets. A release has to add them: apps/saas/public holds the trainee app preview bundle, the assistant avatar images and the wizard assets. If a file is not on disk under public when the release is packaged, it is not in production. The server is started with node apps/saas/server.js. It reads PORT and HOSTNAME.

Build time and run time variables

The Dockerfile and both workflows still pass DATABASE_URL and BETTER_AUTH_SECRET into the build. No code in apps or packages reads either. In dev.yml, BETTER_AUTH_SECRET is only used as the input for deriving the server actions encryption key. The Dockerfile supplies obvious placeholders for both.
SERVICE_AUTH_SECRET must match the core API’s value, or every signed call fails.

The Docker image

frontend/Dockerfile is a two stage build on node:22-bookworm-slim. Builder:
  1. corepack enable, copy the repo, pnpm install --frozen-lockfile.
  2. Take NEXT_PUBLIC_SAAS_URL, NEXT_PUBLIC_SITE_URL and CORE_API_URL as build arguments.
  3. pnpm build (both apps through Turborepo).
  4. pnpm --filter=marketing build, then copy the export into apps/saas/public/mkt and mkt-assets.
Runner:
  1. Copy .next/standalone, .next/static and public.
  2. Set NODE_ENV=production, PORT=3043 and a HOSTNAME that binds all interfaces inside the container.
  3. CMD ["node", "apps/saas/server.js"].
.dockerignore excludes node_modules, .next, .turbo, .git, .github, env files and logs.

Workflows

Three files in .github/workflows. deploy-do.yml passes the public production URLs as build arguments: the app at https://app.byperform.co.il and the API at https://perform-api.otherwise.co.il. It uses a concurrency group per ref, so a newer push cancels an in flight deploy. So a push to dev deploys through Docker. main has no deploy workflow in this repo.

The PM2 flow (dev.yml)

Kept for manual use. In order:
  1. Install, derive NEXT_SERVER_ACTIONS_ENCRYPTION_KEY (a SHA-256 of a fixed prefix plus BETTER_AUTH_SECRET, masked in the log), type-check, build.
  2. Package: standalone output, static files, public, the marketing export, the deploy/ scripts and ecosystem.config.cjs, into one tarball named with a UTC timestamp.
  3. Upload with a checksum comparison on both ends and up to three attempts. A truncated upload can otherwise exit zero and only fail later at extraction.
  4. On the server: extract into releases/<id>/, run deploy/dev/release.sh <id>.
deploy/dev/ scripts: ecosystem.config.cjs defines one PM2 app, byperform-app, running apps/saas/server.js in fork mode with one instance, --env-file=.env, a 1 GB memory restart limit and PORT 3043.

No database step

Neither workflow touches a database. The web app has no schema of its own any more. Auth and billing tables are owned by the core API.

Local production run

dev.sh loads .env and starts node apps/saas/server.js on port 3010. Copy .next/static and public into the standalone tree first if you need assets to load, as the release packaging does.

Checklist before a release

  1. pnpm type-check, pnpm lint and pnpm test pass.
  2. Translations for new keys exist in en and he.
  3. If the mobile app changed, the trainee preview bundle under apps/saas/public/trainee-preview was re-exported. See Trainee app previews.
  4. If the oRPC router changed in the backend, orpc-router.generated.d.ts matches it.
  5. If the plan catalog changed in the backend, packages/payments/config.ts matches it.
  6. The core API release that the web build expects is already deployed. The web app is useless without a compatible API.