CRUMB a card from devarno-cloud

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_URL from 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 JSON

Configuration

  • BFF_INTERNAL_URL (server-only env var, default http://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