frontend/docs/DEPLOYMENT.md says the pm2 workflow runs on push to dev and that the app uses its own database. Both statements are out of date. The pm2 workflow is manual, and the web app has no database connection at runtime.The standalone build
apps/saas/next.config.ts sets output: "standalone" and outputFileTracingRoot to the monorepo root. next build --webpack writes a self-contained server to apps/saas/.next/standalone, with a runnable apps/saas/server.js, the traced workspace packages and a trimmed node_modules.
Standalone output does not include static assets. Every packaging step copies apps/saas/.next/static and apps/saas/public next to the server.
The production server listens on port 3043 on 0.0.0.0. A reverse proxy terminates TLS in front of it.
The marketing site ships inside the app
apps/marketing is a second Next.js app built with output: "export" and assetPrefix: "/mkt". Its static export is copied into the web app’s public folder at build time:
apps/saas/proxy.ts rewrites a request for / to /mkt/index.html when the visitor has no session cookie. Signed-in users and every other path are untouched. In development the same rewrite points at the marketing dev server on port 3001 instead.
pnpm build at the repository root builds both apps (turbo build --filter=saas --filter=marketing).
The trainee preview bundle
The web app also serves a web export of the mobile app under/trainee-preview. It is produced in the mobile repository with pnpm preview:export, which writes into frontend/apps/saas/public/trainee-preview. Neither deploy workflow runs that export. Both build only what is in the web repository, so the preview a coach sees is whatever bundle sits in apps/saas/public/trainee-preview in the commit being deployed. Re-export it when a mobile change should be visible in the preview, and check with git status that the files are tracked before you rely on it. Line 54 of the web .gitignore reads apps/saas/public/trainee-preview/packages/database/prisma/generated/, which looks like two entries joined by a missing line break, so it is not clear from the file whether the folder was meant to be ignored. See Mobile builds.
Docker image workflow
The Dockerfile
1
Builder stage
On
node:22-bookworm-slim with Corepack. Copies the repository and runs pnpm install --frozen-lockfile.Build arguments become environment variables for the build: NEXT_PUBLIC_SAAS_URL, NEXT_PUBLIC_SITE_URL, CORE_API_URL and CORE_API_SERVICE_ID (default web). DATABASE_URL, BETTER_AUTH_SECRET and SERVICE_AUTH_SECRET default to clearly named build-time placeholders that are not used at runtime.Runs pnpm build, then builds the marketing export and copies it into apps/saas/public as described above.2
Runner stage
A clean
node:22-bookworm-slim. Copies the standalone tree, the static assets and the public folder. Sets NODE_ENV=production, PORT=3043, HOSTNAME=0.0.0.0. Runs node apps/saas/server.js.NEXT_PUBLIC_* values are compiled into the client bundle. Changing one means rebuilding the image. Server-only values such as SERVICE_AUTH_SECRET are read at runtime from the container’s environment, so the real values must be supplied by the host’s Compose file.
The workflow
- Build the image with the three production build arguments and push it to the GitHub Container Registry, tagged
latestand with the commit SHA. - Connect to the host over SSH and run
docker compose pull,docker compose up -danddocker image prune -fin the application directory.
docker compose up -d, or revert on dev.
pm2 release workflow
dev.yml builds on the runner with Node 24.
pnpm install --frozen-lockfile.- Derive
NEXT_SERVER_ACTIONS_ENCRYPTION_KEYfrom the auth secret with a fixed prefix and SHA-256, mask it, and export it for the build. Without a stable key, Server Action ids change on every build and open browser tabs break after a release.DEPLOYMENT_VERSIONis set to the commit SHA and feedsdeploymentIdinnext.config.ts. pnpm type-check, thenpnpm build.- Package
release_pkg: the standalone tree,.next/static,public, the marketing export, thedeployfolder andecosystem.config.cjs. Pack and test the tarball. - Upload with
scp, verify the SHA-256 checksum on the host, retry up to three times. - On the host: extract into
releases/<release id>and rundeploy/dev/release.sh.
release.sh, rollback.sh, health-check.sh, prune.sh) work exactly as in the backend. See Deploying the backend. The differences:
release.sh also deletes a pm2 app named byperform-marketing if one exists. Marketing used to run as its own process and now ships inside the web app.
Runtime environment
The running server needs at least:
See Web and mobile environment variables for the full list.
Deploy order with the backend
The web app calls the API on every page. When a change needs both:- Deploy the backend first, with the new endpoint or field added in a backward compatible way.
- Deploy the web app.
- Remove the old endpoint or field in a later backend release, once the mobile app no longer uses it either.
dev in both repositories at the same moment starts two independent workflows with no ordering between them.
After a deploy
/api proxy to the Hono app in the backend and returns OK. It proves the web server can reach the API. Then sign in and load a trainee list, which exercises the signed /v1/web path.