Skip to main content
Version: 57.2.0

Deployment baseline

Every Terreno production deployment needs the same seven foundations. Provider guides (Deploy to GCP, future Vercel guide) map these to specific platforms; this page names them once.

See also the environment variables reference.

Seven baseline requirements

#RequirementWhy Terreno needs itWhen missing
1MongoDB replica setChange streams power realtime and live feature-flag updatesStartup logs: FeatureFlag.watch() failed — live updates require MongoDB as a replica set (feature-flags/src/featureFlagsApp.ts)
2Auth secretsJWT signing and sessions (or Better Auth encryption)Login fails; tokens rejected
3Long-lived backend processSocket.io connections are not request/responseWebsockets drop on platforms with short request timeouts or aggressive scale-to-zero
4Correct originsCORS and Better Auth trustedOrigins must include your web and native schemesBrowser: blocked by CORS; native: auth redirects fail
5Build-time client configEXPO_PUBLIC_* values are inlined into the web bundle at build timeAPI calls go to undefined or localhost after deploy — see Build for web
6Health endpointLoad balancers and orchestrators need readiness probesPlatform marks the service unhealthy and stops routing traffic
7Durable file storageUploads must not live on ephemeral container diskFiles disappear on restart; use GCS/S3 and credentials

Requirements 1, 3, and 5 are the ones teams miss most often.

Symptom details

1 — Standalone MongoDB

Terreno's feature-flag plugin opens a change stream on startup. Without a replica set, live flag sync is disabled and logs:

[feature-flags] FeatureFlag.watch() failed — live updates require MongoDB as a replica set (even single-node).

Use MongoDB Atlas or a single-node replica set for development.

3 — Short-lived serverless timeouts

The RTK/socket client enables reconnection (reconnection: true, reconnectionAttempts: 5 in rtk/src/socket.ts). When the backend drops idle connections (for example Cloud Run's default 300s request timeout), clients reconnect but realtime state may lag until the new socket attaches.

5 — Build-time API URL

If you set EXPO_PUBLIC_API_URL only at runtime on the static host, the bundle still contains whatever was present at bun run export time — often http://localhost:4000. The browser then fails network requests against localhost.

Web output modes

Expo Router supports three web output modes. The choice determines hosting options.

ModeOutputAPI routesSSRHostingTerreno status
singleOne index.html SPANoNoAny static hostCurrent default
staticPer-route HTML filesNoNoAny static hostAvailable; better SEO
serverdist/client + dist/serverYesYes (alpha, SDK ≥ 55)Node/Bun/edge runtimeNot yet — repo catalog is Expo ~54.0.29; see Web SSR and admin SPA

Multi-environment checklist

Staging and production differ in these values (each environment needs its own web build because of EXPO_PUBLIC_API_URL):

ItemWhere to configure
Secret valuesPlatform secret store / Secret Manager
MONGO_URISeparate cluster or database per environment
EXPO_PUBLIC_API_URLSet before bun run export — one build per environment
corsOriginsetupServer({ corsOrigin }) in backend
Better Auth trustedOrigins + BETTER_AUTH_URLBetter Auth config / env
Feature-flag defaultsMongoDB documents or seed scripts

Containerizing the backend

The canonical Dockerfile is example-backend/Dockerfile. It:

  1. Installs workspace dependencies and compiles packages in dependency order.
  2. Builds a Bun-compiled binary for production.
  3. Runs as a non-root user and exposes /health via @terreno/api-health.

Three details consumers get wrong:

  • PORT — Cloud Run and other platforms assign the listen port via PORT. Terreno reads process.env.PORT in api/src/terrenoApp.ts (default 9000 if unset).
  • Non-root — Do not run the container as root in production.
  • Compile order — Workspace packages (@terreno/api, @terreno/test, etc.) must compile before the example backend bundles.

CI builds the image on every PR that touches backend paths (.github/workflows/example-backend-docker.yml).