Deployment¶
For the dev → UAT → production flow, promotion and rollback, see ENVIRONMENTS.md.
Three ways to run it, in increasing order of production-likeness. All use free and open-source software.
1. Local (default)¶
See the README quick start. SQLite at backend/data/fab.db (created on first use) and an
exact NumPy repair-history index built on first query. No configuration needed; set FAB_QDRANT_URL
to use a Qdrant server instead.
2. Docker Compose (production-like)¶
docker compose up --build # http://localhost:8080
docker compose down -v # stop and delete volumes
The stack signs in with one-click demo accounts. Each release also publishes images with a GitHub
build-provenance attestation (not a cosign signature): ghcr.io/pavansky/fab-dispatch-api and
ghcr.io/pavansky/fab-dispatch-ui, tagged with the version and latest. Check where one came from with
gh attestation verify oci://ghcr.io/pavansky/fab-dispatch-api:latest -R pavansky/fab-dispatch.
| Service | Image | Notes |
|---|---|---|
postgres |
postgres:16-alpine | volume pgdata, healthcheck |
qdrant |
qdrant/qdrant:v1.19.0 | volume qdrant |
api |
backend/Dockerfile |
python:3.12-slim, non-root, 2 uvicorn workers, JSON logs, healthcheck on /api/livez. Trusts X-Forwarded-* only from FORWARDED_ALLOW_IPS (default 127.0.0.1; compose sets * because nginx is the only thing that can reach it) |
web |
frontend/Dockerfile |
unprivileged nginx (nginxinc/nginx-unprivileged, port 8080, healthcheck) serving the Vite build. Proxies /api with SSE buffering off, caches hashed assets for a year and sends the security headers from every location |
3. Vercel + Supabase (+ Qdrant Cloud, optional)¶
Current production: https://fab-dispatch.vercel.app (Vercel project fab-dispatch, auto-deploys from
main). The database is Supabase fab-dispatch on the free plan, created through Vercel → Storage →
Supabase and hosted in Washington DC (iad1), the same region as the API functions, to keep query
latency low. The integration injects POSTGRES_URL, which the app reads. Access is behind Vercel
Authentication (deployment protection) until it's opened to reviewers.
vercel.json defines two Services in one project: web (Vite, frontend/) and api (FastAPI,
backend/, entrypoint app.main:app). /api/* goes to the API and everything else to the UI. The API
receives the original /api/... path, so routes need no changes.
Steps¶
- Supabase (free tier): create a project, then copy the transaction pooler connection string (port 6543) from Project Settings → Database. The schema is created automatically on first request.
- Qdrant Cloud (optional, free tier): create a cluster and copy its URL and API key. Without it, the repair index runs in memory in each instance (rebuilt on cold start in about 0.6 s).
- Vercel: import the repository (or run
vercel linkthenvercel deploy), and set the environment variables:
| Variable | Value |
|---|---|
FAB_DATABASE_URL |
Supabase pooled URL. Or skip it and connect Supabase through Vercel's Storage integration: the app also reads the POSTGRES_URL it injects, and strips the supa= parameter libpq rejects |
FAB_QDRANT_URL, FAB_QDRANT_API_KEY |
optional: repair search on a Qdrant server (exact NumPy search otherwise) |
FAB_TENANT_DATABASES |
optional: {"<fab id>": "env:VAR"} gives that fab its own database, reading the URL from a variable a hosting integration injects (so the secret is never copied into this one); a literal "postgresql://..." URL also works |
FAB_STREAM_TICKET_SECRET |
signs the 90 s tickets that open a live stream. Optional when SUPABASE_JWT_SECRET (or a non-default FAB_AUTH_SECRET) is set, which is then reused; required otherwise on any deployment with more than one instance, or tickets from one instance are refused by another |
FAB_INGEST_TOKENS |
optional: {"<fab id>": "<sha256 of token>"} enables equipment ingestion for that fab |
FAB_CORS_ORIGINS |
not needed: same origin |
- Deploy and check
https://<app>/api/health→{"status":"ok","db_ok":true}.
Serverless behaviour, by design¶
- Without
FAB_DATABASE_URLthe app still boots, using SQLite in/tmpper instance. That's fine for a demo, but live shifts won't be shared across instances. Set it for production. - SSE responses close after 25 s and browsers reconnect with
Last-Event-ID. This stays well under the function duration limit (maxDuration: 60). - Benchmark requests are per preset, so each finishes in well under a minute.
- Rate limits have two layers. A token bucket per instance (in memory) absorbs bursts; for
signed-in users a per-minute counter in the default database then holds the quota across every
instance. Unauthenticated endpoints (
/api/help/search, ingestion) use the in-memory bucket only, so they never cost a database write. If the counter can't be reached, cheap endpoints let the request through; expensive ones (/benchmark,/optimality-gap,POST /shifts, the assistant when a paid model is on) answer 503 withRetry-After. Vercel's firewall is still the place for edge rate limiting of unauthenticated traffic. - Bundle: the runtime requirements install to about 320 MB (measured; SciPy and gRPC are the largest), under Vercel's 500 MB Python limit.
If Services (beta) isn't enabled on the account¶
Deploy two projects from the same repository instead:
- API project: root backend/. Set the variables above, plus FAB_CORS_ORIGINS=["https://<web-domain>"].
- Web project: root frontend/, with VITE_API_BASE=https://<api-domain>/api at build time.
Operations¶
- Liveness:
GET /api/livez(no dependencies). Readiness:GET /api/health(database reachable). - Every response carries
X-Request-ID, which is also in the JSON log line, so you can trace a user report to the exact request. - Cache:
GET /api/cache/stats(signed in) gives in-process hit and miss counts. BumpENGINE_VERSIONinapp/cache.pywhenever solver behaviour changes. - Database growth:
shift_eventsandplan_cacheare append-only. Vercel Cron callsPOST /api/internal/prunedaily (production only; see the retention section of ENVIRONMENTS.md for what it deletes and how to prune UAT). Outside Vercel, call that endpoint from any scheduler withAuthorization: Bearer $CRON_SECRET.