Deploy (production)
Purpose
Deploy VibeSwitch to a real environment — a VM, a container, Cloud Run, or a Node.js-capable PaaS — with sensible defaults for auth, secrets, and the SPA build. This guide covers what runs, what serves what, which environment variables matter, and where things typically break in production that don't break locally.
Prerequisites
- Required: A runtime that can run Node.js 20+ (container, VM, Cloud Run, Render, Fly, etc.).
- Required: A secrets mechanism — real env vars, a secret manager (Google Secret Manager, AWS Secrets Manager, Vault), or your platform's equivalent. Not
.envfiles checked into the repo. - Required: A way to route traffic to the server (a reverse proxy or your platform's built-in router).
- Strongly recommended: Google Identity Platform / Firebase Auth if the app is reachable from the public internet.
- Optional: A custom domain and TLS cert (usually supplied by your platform or a proxy like Cloudflare).
Inputs
- Server env (at runtime, not in the image):
NEWSAPI_API_KEY— news ingestion.ANTHROPIC_API_KEY— signal extraction + narratives.OPENAI_API_KEY— audio transcription (only if audio is enabled).AUTH_REQUIRED=true— force JWT verification on protected routes.FIREBASE_PROJECT_ID— required when auth is on.SQLITE_PATH— point this at a persistent volume.PORT— the port to bind (most platforms set this automatically).WHATSAPP_VERIFY_TOKEN,WHATSAPP_ACCESS_TOKEN,WHATSAPP_PHONE_NUMBER_ID,WHATSAPP_ALLOWED_GROUP_IDS— only if WhatsApp ingestion is enabled.
- Client build env (build-time only, baked into the bundle):
VITE_FIREBASE_API_KEYVITE_FIREBASE_AUTH_DOMAINVITE_FIREBASE_PROJECT_ID
- ADC credentials: a runtime service account attached to the workload so the server can verify Firebase tokens. Avoid JSON key files in production.
Outputs
- A single Node process serving both
/api/*and the static SPA fromclient/dist/. - Explicit auth posture: either
AUTH_REQUIRED=false(public) orAUTH_REQUIRED=truewith working Firebase wiring. - A persistent SQLite file at
SQLITE_PATH, on a volume that survives restarts. - Observable endpoints:
/api/openapi.json,/api/auth/config,/api/docs/indexall respond with200to unauthenticated callers.
Constraints
- Never embed server secrets in the client build. Only
VITE_*values end up in the browser bundle. Firebase web config (API key, auth domain, project ID) is designed to be public — your server enforcement is what keeps the app secure, not the secrecy of those values. VITE_*is baked at build time. Rotating auth domain or project ID means rebuilding and redeploying the client, not restarting the server.- SQLite needs a real disk. Containers without a mounted volume lose the DB on every restart. Mount a persistent volume (EBS, Cloud Run with attached volume, Fly volumes, etc.) and point
SQLITE_PATHat it. - CI should not ship keys. Your deploy pipeline should pull secrets from a secret manager, not from CI variables for long-lived keys.
- One writer per SQLite file. Don't horizontally scale behind the same volume — pick a single instance, or migrate off SQLite first (outside this guide's scope).
Examples
Build the SPA
npm run client:build
Expected: client/dist/ is produced, containing index.html and hashed assets. The Fastify server serves this directory for all non-/api routes.
Run the server
npm run start
Expected: visiting the root URL loads the SPA; /api/* handlers respond. On a public host, always put this behind TLS.
Verify the deployment
curl -sS -o /dev/null -w "%{http_code}\n" https://YOUR_HOST/api/openapi.json
Expected: 200.
curl -sS https://YOUR_HOST/api/auth/config
Expected: {"authRequired":true} if you've enabled auth, otherwise {"authRequired":false}. This endpoint is intentionally unauthenticated — the client uses it to decide whether to prompt for sign-in.
A sensible deploy recipe
- Bake the client during CI:
cd clientVITE_FIREBASE_API_KEY=... VITE_FIREBASE_AUTH_DOMAIN=... VITE_FIREBASE_PROJECT_ID=... npm run build
- Build the server image (or zip) with
node_modulesandclient/dist/included. - Inject runtime secrets (server-side env vars) from your platform's secret manager.
- Attach a service account with permissions to verify Firebase tokens (no need for broad roles).
- Mount a persistent volume and set
SQLITE_PATHto a path inside it. - Smoke test: the three curl commands above should all return
200/json.
Troubleshooting
- Sign-in loop or "config error" on the login screen
- Check: the client was built with all three
VITE_FIREBASE_*values. Open devtools → Network → look at the Firebase auth request. - Fix: set the values, rebuild
client/dist, redeploy. Don't try to "inject" them at runtime — the bundle is already built.
- Check: the client was built with all three
- Google sign-in shows "unauthorized domain"
- Check: your production domain is listed under Firebase → Authentication → Settings → Authorized domains.
- Fix: add the domain and wait a minute for it to propagate.
- API returns 401 for authenticated requests
- Check: the server can verify ID tokens. In Cloud Run, this requires the runtime service account to have enough privilege for
firebase-admin; locally or in other platforms,GOOGLE_APPLICATION_CREDENTIALSmust point at a valid service account JSON. - Fix: attach a service account in production. Avoid shipping JSON keys — use ADC.
- Check: the server can verify ID tokens. In Cloud Run, this requires the runtime service account to have enough privilege for
- Reports disappear after each deploy
- Check:
SQLITE_PATHpoints at ephemeral storage (the container's writable layer). - Fix: mount a persistent volume and point
SQLITE_PATHat it. Verify with a restart.
- Check:
- Client bundle references old Firebase project after env rotation
- Check: the build step ran with new
VITE_*values. - Fix: clear any build cache, rebuild, redeploy. Vite embeds the values into JS at build time.
- Check: the build step ran with new
- Docs site (Docusaurus) build fails on Cloudflare Pages
- Check:
NODE_VERSION=20is set in the Pages environment; the build command matchesdocs-site/README.md(usuallynpm ci && npm run buildinsidedocs-site/). - Fix: align Node version, redeploy from the latest commit. See Common failures.
- Check:
- High memory on the server
- Check: whether you're running analysis in-process on very large inputs.
- Fix: split long audio, cap articles per run, and run heavy pipelines from a CLI off-hours. See Cost controls.