Skip to main content

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 .env files 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_KEY
    • VITE_FIREBASE_AUTH_DOMAIN
    • VITE_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 from client/dist/.
  • Explicit auth posture: either AUTH_REQUIRED=false (public) or AUTH_REQUIRED=true with 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/index all respond with 200 to 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_PATH at 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

  1. Bake the client during CI:
    cd client
    VITE_FIREBASE_API_KEY=... VITE_FIREBASE_AUTH_DOMAIN=... VITE_FIREBASE_PROJECT_ID=... npm run build
  2. Build the server image (or zip) with node_modules and client/dist/ included.
  3. Inject runtime secrets (server-side env vars) from your platform's secret manager.
  4. Attach a service account with permissions to verify Firebase tokens (no need for broad roles).
  5. Mount a persistent volume and set SQLITE_PATH to a path inside it.
  6. 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.
  • 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_CREDENTIALS must point at a valid service account JSON.
    • Fix: attach a service account in production. Avoid shipping JSON keys — use ADC.
  • Reports disappear after each deploy
    • Check: SQLITE_PATH points at ephemeral storage (the container's writable layer).
    • Fix: mount a persistent volume and point SQLITE_PATH at it. Verify with a restart.
  • 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.
  • Docs site (Docusaurus) build fails on Cloudflare Pages
    • Check: NODE_VERSION=20 is set in the Pages environment; the build command matches docs-site/README.md (usually npm ci && npm run build inside docs-site/).
    • Fix: align Node version, redeploy from the latest commit. See Common failures.
  • 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.