Same-Origin BFF Proxy Pattern
so1 intermediate 5 min read
ELI5
The browser only ever knocks on the front door at console.example.com. Behind that door, a clerk (/api/bff/[...path]) walks every request to the kitchen at BFF_INTERNAL_URL and brings the answer back, so the diner never knows the kitchen’s address.
Technical Deep Dive
Why same-origin
Per ADR-002, the BFF is reached through a Next.js route handler (src/app/api/bff/[...path]/route.ts) on the same hostname as so1-rover. This:
- Makes session cookies (
so1.session_token/__Secure-so1.session_token) automatically present. - Eliminates CORS preflight overhead.
- Hides
BFF_INTERNAL_URLfrom client JavaScript.
Request Path
sequenceDiagram participant B as Browser participant N as Next.js (so1-rover) participant H as Hono BFF (so1-control-plane-api) participant U as Upstream (GitHub/n8n/MCP) B->>N: GET /api/bff/catalog (cookie: so1.session_token) N->>N: read session, mint Authorization: Bearer <token> N->>H: GET ${BFF_INTERNAL_URL}/api/catalog H->>H: middleware: auth, requestId, logging H->>U: GET upstream resource U-->>H: payload H-->>N: JSON + X-Request-Id N-->>B: passthrough JSONConfiguration
BFF_INTERNAL_URL(server-only env var, defaulthttp://localhost:3001) selects the upstream.- The route handler is the only place that reads the session and converts it into a Bearer token; client code calls relative URLs only.
Failure Surfaces
flowchart TD start[fetch /api/bff/x] --> auth{session valid?} auth -- no --> r401[401 from proxy] auth -- yes --> fwd[forward to BFF] fwd --> upok{upstream 2xx?} upok -- yes --> pass[passthrough body] upok -- no --> envel[wrap as ErrorEnvelope]Key Terms
- Route handler → a Next.js file that exports HTTP method functions; runs on the server.
- Bearer token → opaque session credential placed in
Authorization: Bearer …. - Passthrough → relaying upstream status and body unchanged unless an error envelope is needed.
Q&A
Q: Why not call the BFF directly with fetch('https://api.example.com/...') from the browser?
A: That requires CORS, exposes the BFF hostname, and forces the auth token into client-side JS. Same-origin avoids all three.
Q: What does the proxy add to each forwarded request?
A: Authorization: Bearer <session token> derived server-side from the cookie; everything else is forwarded.
Q: Where is the proxy implemented in the codebase?
A: src/app/api/bff/[...path]/route.ts in so1-rover.
Examples
useCatalogRepos in src/lib/hooks.ts issues fetch('/api/bff/catalog'). It does not know whether the BFF is on localhost:3001, a Cloud Run URL, or a sibling Vercel project — that is BFF_INTERNAL_URL’s job.
neighbors on the map
- Request Routing & Edge Resolution debugging why a slug returns 404 instead of redirecting
- Proxy Mode implementing a white-label short link
- purr-api Layered Architecture adding a new feature to purr-api