Proxy (BFF)
HTTP + WebSocket reverse proxy
The Proxy is a backend-for-frontend you run alongside your web app. It forwards both HTTP and WebSocket traffic to your services, rewrites paths, and — most importantly — injects auth tokens from httpOnly cookies so the browser never holds a credential.
It solves the awkward parts of a split frontend/IDP deployment: cross-origin cookies, WebSocket auth (which can't send custom headers from the browser), and silent token refresh on 401. You describe targets; the proxy handles cookie parsing, JWT extraction, path matching and reconnection.
Spin up both protocols at once with startWsProxyServer / a ProxyServerConfig, or mount the HTTP and WS handlers individually inside an existing server.
The proxy server#
ProxyServerConfig binds an HTTP proxy and a WS proxy under one port. createHttpProxyHandler and createWsProxyHandler expose the same logic as standalone handlers when you want to embed them.
1import { startWsProxyServer } from "nucleus-core";2 3startWsProxyServer({4 port: 4000,5 http: {6 targets: [7 {8 url: process.env.IDP_URL!,9 paths: ["/auth/*", "/api/*"],10 injectTokenFromCookie: { cookieName: "access_token", headerName: "authorization" },11 tokenRefresh: { enabled: true, idpBaseUrl: process.env.IDP_URL!, retryOn401: true },12 },13 ],14 },15 ws: {16 targets: [17 {18 url: process.env.IDP_URL!,19 paths: ["/api/events/*"],20 injectTokenFromCookie: { cookieName: "access_token", queryParam: "token" },21 },22 ],23 },24});port / hostnamenumber / stringOptionalWhere the proxy listens. WS defaults to its own port when run separately.
4000 / 0.0.0.0devbooleanOptionalDevelopment mode — enables HMR pass-through and looser origin handling for local work.
httpHttpProxyConfigOptionalHTTP forwarding config: an array of targets plus onRequest/onResponse/onError hooks for logging and metrics.
wsWsProxyConfigOptionalWebSocket forwarding config: targets, optional HMR allowance, unknown-path rejection, ping interval and lifecycle hooks.
envPath / onReady / onErrorstring / functionsOptionalOptional env file to load, plus callbacks fired when the server is listening or errors.
Targets & routing#
Each target maps a set of glob paths to an upstream URL. paths are matched with glob-to-regex, pathRewrite reshapes the forwarded path, and changeOrigin rewrites the Host header. The same shape exists for HTTP and WS, with a few protocol-specific extras.
urlstringRequiredThe upstream service this target forwards to.
pathsstring[]RequiredGlob patterns that route to this target (e.g. '/auth/*', '/api/*'). First match wins.
Example: ["/auth/*", "/api/*"]
pathRewriteRecord<string, string>OptionalRewrite matched paths before forwarding (e.g. strip a '/api' prefix the upstream doesn't expect).
changeOriginbooleanOptionalRewrite the Host header to the target's origin — needed by many upstreams.
headersRecord<string, string>OptionalStatic headers added to every forwarded request on this target.
Cookie → token injection & refresh#
This is why the proxy exists. injectTokenFromCookie lifts a token out of an httpOnly cookie and re-presents it as a header (HTTP) or query param (WS, which can't set headers from the browser). injectUserIdFromJwt decodes the JWT and forwards the user id. tokenRefresh transparently refreshes an expired token and retries the request.
injectTokenFromCookie.cookieNamestringOptionalThe cookie to read the token from, with optional fallbackCookieNames for older names.
injectTokenFromCookie.headerName / queryParamstringOptionalWhere to put the token downstream: a header for HTTP (e.g. 'authorization'), or a query param for WebSocket handshakes that can't carry custom headers.
injectUserIdFromJwt{ cookieName; headerName? }OptionalDecode the JWT in a cookie and forward its subject as a header — lets upstreams trust an identity without re-parsing tokens.
tokenRefresh.enabled / idpBaseUrlboolean / stringOptionalTurn on silent refresh and point it at the IDP. The proxy calls the refresh endpoint when needed.
tokenRefresh.retryOn401 / maxRefreshRetriesboolean / numberOptionalOn a 401, refresh the token and replay the original request, bounded by maxRefreshRetries.
tokenRefresh.refreshCookieName / accessCookieNamestringOptionalWhich cookies hold the refresh and access tokens, so rotated values can be re-set after a refresh.
Related sections