backend repository is a single Express 5 process that serves authentication, billing, the domain API, webhooks and background workers. The web app and the mobile app are clients of it.
The big picture
What runs inside the core API process
backend/apps/core-api/src/server.ts builds one AppContext and passes it to every module. The context holds the validated env, the Prisma client, the BullMQ queue factory, the SmartSend client, the logger, the Redis nonce store, the service secrets and the better-auth instance.
backend/apps/core-api/src/app.ts mounts everything in this order:
After the HTTP server starts listening,
server.ts starts the workers and schedulers in the same process: task scheduler, update form scheduler, subscription reconcile scheduler, hook delivery worker, AutoFit import worker, push receipt worker, agent reply worker, flow run and flow sweep workers, plan release scheduler and demo activity scheduler. There is no separate worker deployment.
The Hono app under /api
backend/packages/api/src/index.ts defines a Hono app with base path /api:
/api/auth/**goes toauth.handlerfrom@repo/auth. This is the better-auth server./api/webhooks/paymentsgoes to the payments webhook handler from@repo/payments./api/healthreturnsOK.- Paths containing
/rpc/go to the oRPC handler. Other paths go to the OpenAPI handler.
honoGateway (src/middleware/hono-gateway.ts) rebuilds a Fetch Request from the Express request using the captured raw body, calls honoApp.fetch, and copies the response back. It writes Set-Cookie headers one by one, because joining them with a comma produces a cookie the browser discards.
How the web app reaches the API
The web app has two paths to the backend. Both run on the Next.js server, never in the browser.- Session path (/api)
- Domain path (/v1/web)
The catch-all route
frontend/apps/saas/app/api/[[...rest]]/route.ts calls proxyToCoreApi from modules/shared/lib/bff-proxy.ts. It forwards the request to CORE_API_URL with the same path and query string, forwards cookies, strips hop-by-hop headers, uses redirect: "manual", and streams the response back with every Set-Cookie preserved.This keeps auth cookies on the web app’s own origin while better-auth and oRPC run in the backend. There is no HMAC on this path. The session cookie authenticates it.If the API is unreachable the proxy answers 503 with code SERVICE_UNAVAILABLE and a message that tells you to start the backend.Lanes of the /v1 router
buildV1Router in backend/apps/core-api/src/modules/index.ts groups routes into lanes. Each lane has its own authentication.
The bearer lane verifies a session in one of two ways. With
SESSION_VERIFY_INPROCESS=true it calls auth.api.getSession in process. Otherwise it makes an HTTP request to BETTER_AUTH_URL. Since better-auth now runs in the same process, the in-process mode avoids a network hop back to itself.Data stores
- PostgreSQL. One database with two schemas.
authholds the better-auth tables (users, sessions, organizations, members, purchases).publicholds the domain tables. One Prisma client from@repo/databasereads both. See Database migrations. - Redis. Used by BullMQ for queues and by
createRedisNonceStorefor replay protection. Queue names are constants onPerformQueueinbackend/packages/queue/src/index.ts. - Object storage. S3-compatible storage, configured through the
R2_*variables, for images, videos and generated PDFs.
Response envelope
Every/v1 response uses the envelope defined in backend/packages/types/src/index.ts:
INTERNAL, BAD_REQUEST, UNAUTHORIZED, FORBIDDEN, NOT_FOUND, CONFLICT, RATE_LIMITED, VALIDATION and SERVICE_UNAVAILABLE. The request id comes from the X-Request-Id header when the caller sends one, or a new ULID.
Ownership boundaries
- The backend owns all persistent data and all secrets for third parties.
- The web app owns presentation, translations, and the server actions that call the API. It holds no database connection at runtime.
- The mobile app owns presentation and device features such as push notifications, HealthKit, Health Connect, widgets and the iOS Live Activity. It talks only to the trainee lane.
- The web app serves two static bundles from its
publicdirectory: the marketing landing page under/mkt, and a web export of the trainee app under/trainee-previewthat coaches see as a live preview.