Stateless BFF Deployment Topology
so1 intermediate 4 min read
ELI5
Each BFF box is interchangeable — no box remembers the last visitor. So a load balancer can shuffle traffic between any number of boxes, and adding more boxes is the entire scaling plan.
Technical Deep Dive
Topology
---title: "so1 Deployment"---flowchart TD user(("<b>Operator</b>")):::person rover["<b>so1-rover</b><br/>Next.js 16 — Vercel/Node"]:::system subgraph api ["**so1-control-plane-api**"] lb["<b>Load Balancer</b>"]:::system b1["<b>BFF instance 1</b><br/>Hono container"]:::system b2["<b>BFF instance N</b><br/>Hono container"]:::system end secrets["<b>Secrets Manager</b><br/>KMS / Vault"]:::ext integrations["<b>GitHub / n8n / MCP</b>"]:::ext user -- "HTTPS" --> rover rover -- "HTTPS, same-origin" --> lb lb -- "HTTP" --> b1 lb -- "HTTP" --> b2 b1 --> secrets b2 --> secrets b1 --> integrations b2 --> integrations
classDef person fill:#1c1c24,stroke:#e85d3e,color:#f0ece6 classDef system fill:#1c1c24,stroke:#d4a574,color:#f0ece6 classDef ext fill:#141419,stroke:#8b7e74,color:#f0ece6,stroke-dasharray: 4 3 classDef db fill:#1c1c24,stroke:#d4a574,color:#f0ece6 classDef container fill:#1c1c24,stroke:#d4a574,color:#f0ece6Environment Matrix (ADR-002)
| Env | Frontend | BFF | Notes |
|---|---|---|---|
| local dev | npm run dev (rover, port 3000) | npm run dev (api, port 3001) | BFF_INTERNAL_URL=http://localhost:3001 |
| staging | container | container (Cloud Run / ECS) | env-specific secrets injected |
| production | container | same container image | only env vars differ |
The same Docker image is used across staging and production; Dockerfile lives in so1-control-plane-api.
Statelessness Rule
ADR-002 §6: “BFF is stateless; no in-memory caches or sessions.” Implications:
- Any state-bearing concern (idempotency store from so1-009, log persistence from so1-008, cache) must externalise to Redis / DB / S3.
- Adding sticky sessions to the load balancer is a smell — fix the offending state instead.
- Horizontal scaling is N→N+1 at any time without warm-up.
Same-Origin Constraint
The frontend and BFF share an origin (e.g., console.example.com with the BFF mounted at /api/). Per ADR-002 this enables secure cookies and avoids CORS — see so1-002 for the proxy that makes this concrete.
Trade-off
flowchart LR cheap[stateless = cheap horizontal scale] ext[every cache/store is external] hop[extra hop browser→BFF→upstream] cheap --> ext cheap --> hopADR-002 names the ~10–50ms per-request overhead from the extra hop as the accepted cost.
Key Terms
- Stateless → instance has no per-user memory between requests.
- Same-origin → identical scheme/host/port for frontend and BFF.
- Image parity → staging and production run the same artefact, differing only in injected configuration.
Q&A
Q: Could the BFF cache adapter responses in memory for 60s? A: That violates the stateless rule and produces inconsistent reads across instances. Use a shared cache (Redis) or rely on upstream caching headers.
Q: Why is BFF_INTERNAL_URL server-only and not NEXT_PUBLIC_…?
A: Hiding the BFF hostname from client JS keeps the same-origin proxy contract intact (so1-002).
Q: Where is the production secrets manager defined? A: ADR-001 §“Secrets Manager Strategy (Deferred)” — the choice (KMS / Vault / Google Secret Manager) is deferred to TASKSET 5.
Examples
Doubling traffic: add more BFF replicas behind the load balancer; no migration, no warm-up, no session affinity. Idempotency lookups (so1-009) and log persistence (so1-008) hit shared external stores, so any replica can serve any request.
neighbors on the map
- Deployment Topology & Proxy Conflict Resolution setting up a new environment (kitten/cat/lion)
- LORE+CAIRNET Deployment Topology & Service Map understanding the LORE deployment architecture
- FNP Kubernetes Multi-Region Architecture deploying FNP across multiple regions