OBP-API Proxy Pattern
April 8, 2026 · View on GitHub
Overview
The OBP-Portal's SvelteKit Node backend acts as an authenticated proxy to the OBP-API. The browser never talks to the OBP-API directly. This design:
- Keeps OAuth tokens server-side — the access token is stored in the session and never sent to the browser
- Provides a uniform API — the Portal's API mirrors the OBP-API exactly, no reshaping
- Enables protocol bridging — the Node backend can speak gRPC to the OBP-API and SSE to the browser
┌─────────┐ ┌──────────────────┐ ┌──────────┐
│ Browser │ HTTP │ SvelteKit Node │ HTTP │ OBP-API │
│ │ ──────> │ (proxy) │ ──────> │ │
│ │ <────── │ + OAuth token │ <────── │ │
└─────────┘ └──────────────────┘ └──────────┘
│ │
SSE │ gRPC │
<──────│ ────────────────────────>│
Generic proxy: /proxy/obp/[...path]
File: src/routes/proxy/obp/[...path]/+server.ts
A single catch-all route that handles GET, POST, PUT, DELETE, and PATCH. It:
- Checks the user is authenticated (returns 401 if not)
- Reads the OBP path from the URL (
/proxy/obp/v6.0.0/foo→/obp/v6.0.0/foo) - Forwards query parameters as-is
- Forwards the request body as-is (for POST/PUT/PATCH)
- Adds the
Authorization: Bearer {token}header - Returns the OBP-API response unmodified (same status code, same JSON body)
Usage from client-side code
// GET messages
const res = await fetch(`/proxy/obp/v6.0.0/chat-rooms/${roomId}/messages`);
const data = await res.json(); // { messages: [...] } — same shape as OBP-API
// POST a message
const res = await fetch(`/proxy/obp/v6.0.0/chat-rooms/${roomId}/messages`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: 'Hello', message_type: 'text' })
});
// DELETE a reaction
await fetch(`/proxy/obp/v6.0.0/chat-rooms/${roomId}/messages/${msgId}/reactions`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ emoji: '👍' })
});
Error handling
Errors from the OBP-API are passed through with the original status code and JSON body. The proxy adds two additional error cases:
- 504 Gateway Timeout — the OBP-API did not respond within 15 seconds
- 502 Bad Gateway — the OBP-API is unreachable (connection refused, DNS failure, etc.)
These use the same { code, message } format as OBP-API errors.
Security
- The proxy only works for authenticated users — unauthenticated requests get 401
- The OAuth token is added server-side and never exposed to the browser
- The proxy does not restrict which OBP-API paths can be called — this relies on the OBP-API's own authorization (roles, entitlements) to control access
Dedicated routes: /backend/...
Some features need more than simple auth forwarding:
gRPC → SSE bridge (/backend/chat/[chatRoomId]/stream)
The OBP-API streams chat events via gRPC (HTTP/2 binary framing). Browsers can't speak gRPC natively. This route:
- Opens a gRPC stream to the OBP-API's
ChatStreamService.StreamMessages - Converts each gRPC event to JSON
- Pushes events to the browser via Server-Sent Events (SSE)
- Handles reconnection and cleanup
This cannot be a simple proxy because the transport protocol changes (gRPC → SSE).
When to use which
Use the generic proxy (/proxy/obp/...) for:
- Any standard REST call (GET, POST, PUT, DELETE) to the OBP-API
- Endpoints where the request and response should match the OBP-API exactly
Use a dedicated route (/backend/...) for:
- Protocol bridging (gRPC → SSE, WebSocket, etc.)
- Aggregating multiple OBP-API calls into one response
- Server-side validation or transformation that can't be done client-side
- Endpoints that don't map to a single OBP-API call
Adding a new proxied endpoint
No Portal code changes needed. Just call /proxy/obp/ followed by the OBP-API path from the client:
// Call any OBP-API endpoint through the proxy
const res = await fetch('/proxy/obp/v6.0.0/banks');
The proxy handles auth, timeout, and error passthrough automatically.