Backend API
The backend is a FastAPI application running on Fly.io. It exposes a single streaming chat endpoint (POST /chat) and handles all AI processing, qualification logic, RAG retrieval, and human handoff dispatch server-side.
Platform
| Parameter | Value |
|---|---|
| Platform | Fly.io Machines |
| Region | Frankfurt (fra) |
| Machine size | shared-cpu-1x, 512 MB RAM |
| Instances | 0–1 (auto-scales to zero when idle) |
| Idle suspend | Machine suspends after ~5 minutes of inactivity |
| Cold-start latency | ~500–700 ms (suspend → first request served) |
| Internal port | 8080 |
Cold starts are expected and within the 3-second first-token budget for first-turn requests. Subsequent turns in an active session are served from a warm machine. If sustained low latency under concurrent load becomes a requirement, see ADR-006 for the scaling configuration steps (no code changes required).
Health Endpoints
These endpoints are used by Fly.io’s health check system and must not be removed.
| Endpoint | Purpose | Expected response |
|---|---|---|
GET /health |
Process liveness — Docker HEALTHCHECK | {"status": "ok"} — 200 always, if the process is up |
GET /ready |
Application readiness — Fly.io HTTP check | {"status": "ready"} — 200 when ready; 503 during startup or shutdown |
Fly.io polls /ready every 30 seconds with a 5-second timeout and a 30-second grace period after machine start. Three consecutive failures trigger a machine restart.
OpenAPI docs are available at /docs in all environments (disabled at /redoc in production).
Request Limits
- Concurrency: 20 requests soft limit, 25 hard limit per machine. Requests above the hard limit receive a 503.
- Rate limiting: The
/chatendpoint is rate-limited by session ID (implemented withslowapi). Excess requests receive a 429 with aretry_after_secondsfield. - Message length: 2,000 characters maximum per message.
- Per-session token budget: Configurable via environment variable (default 16,000 cumulative tokens). Sessions that exceed the budget receive a graceful closure message.
Deploy Pipeline
Trigger: Automatic on push to main touching backend/**, shared/**, data/**, pyproject.toml, or uv.lock.
Pipeline stages (sequential — each gates the next):
1. Unit tests → pytest (backend/tests/, excluding acceptance/)
2. DB migrations → uv run --package database python -m database.migrate
3. Docker build & push → builds from repo root, pushes to Fly.io registry
4. Deploy → flyctl deploy (image pinned by run_number + short SHA)
5. Smoke test → GET /ready with retry (6 attempts × 10-second delay)
6. Acceptance tests → pytest backend/tests/acceptance/ against the live URL
A failure at steps 1–4 leaves the previous deployment active. Steps 5–6 run after deploy; a smoke test failure will surface but the deploy has already happened — investigate immediately.
To trigger manually: workflow_dispatch on deploy-backend.yml from the GitHub Actions UI.
Docker Image
- Registry: Fly.io private container registry
- Image tag format:
{run_number}-{short_sha}(e.g.42-a1b2c3d) latesttag: also pushed on every deploy- Build context: repository root (required for uv workspace resolution)
- Dockerfile:
backend/Dockerfile— two-stage build; runtime stage runs as non-rootappuser
Rollback
To roll back to a previous version:
flyctl deploy --image registry.fly.io/<app-name>:<previous-tag> --config backend/fly.toml
Find previous tags in the GitHub Actions run history (each run logs the image tag) or in the Fly.io registry. The app-name is managed by the team and not published here.
Alternatively, re-run the GitHub Actions workflow from a previous commit using the workflow_dispatch trigger with that commit checked out.
Operational Commands
These commands require the Fly CLI (flyctl) and a valid FLY_API_TOKEN. The app name is stored in the team’s private runbook.
# Stream live logs
flyctl logs -a <app-name>
# Open an interactive shell on the running machine
flyctl ssh console -a <app-name>
# List deployed machines and their status
flyctl status -a <app-name>
# List image tags in the registry
flyctl releases -a <app-name>
# Add or rotate a secret (restarts the machine)
flyctl secrets set KEY=value -a <app-name>
# Remove a secret
flyctl secrets unset KEY -a <app-name>
Configuration
All runtime configuration is stored as Fly.io secrets. No secret or credential should ever be committed to source control or added to this documentation.
For the full list of environment variables, their types, and default values, see:
backend/.env.examplein the repository — all variables with descriptions- Infrastructure Requirements TRD — required variables checklist and per-component tables
Variable categories (all stored as Fly.io secrets):
| Category | Examples of what is stored |
|---|---|
| LLM provider | API key, model name, stream timeout, token budget |
| RAG / embeddings | API key, relevance threshold, top-K, table name |
| Database | Connection string with SSL |
| Slack handoff | Webhook URL, bot token |
| Email fallback | SMTP host, credentials, recipient address |
| Business hours | Timezone (IANA), start/end hour, same-day cutoff |
| CORS | Allowed origin (the host website domain) |
| LLM observability | Langfuse public key, secret key, host |
After rotating a secret, the machine restarts automatically. Verify recovery via GET /ready.
Load Testing
Load tests verify the backend meets the Time to First Token (TTFT) performance requirement under realistic concurrent load. This is a manual Phase 5 DoD gate — not triggered automatically.
Tool: k6 (Grafana), script at backend/tests/load/load-test.js
Thresholds:
| Metric | Threshold |
|---|---|
| p95 TTFT | < 3,000 ms |
| Error rate | < 5% |
TTFT is measured as TTFB (http_req_waiting) — the time from request sent to first byte received. For a streaming SSE endpoint this equals time-to-first-token and is the standard k6 proxy.
Test configuration:
- 10 virtual users (VUs), 2-minute ramp-up → 10-minute sustained → 30-second ramp-down
- Each VU simulates 5–7 conversation turns with 15–30 second think-time between turns (realistic cadence)
- Message mix: 60% RAG-triggering queries, 40% non-RAG queries
- Machine is pre-warmed to 1 instance before the test run to prevent cold-start latency from contaminating TTFT measurements
How to run:
- Go to the repository’s Actions tab.
- Select the
load-test.ymlworkflow. - Click Run workflow and provide the
base_urlinput (the API base URL to test against). - The workflow pre-warms the machine, runs the k6 test, and uploads a
results.jsonartifact (30-day retention).
k6 exits with code 1 if any threshold fails, which fails the workflow job.
If the test fails:
- Review the k6 step output for which threshold failed and the actual p95/error-rate values.
- If
p95 TTFTexceeds 3,000 ms under 10 VUs: scale the machine — setmin_machines_running = 1andmax_machines_running = 2inbackend/fly.toml. No code changes required. See ADR-006 for the full scaling criteria.