bffStream
A Next.js route handler that relays the API's event stream with the session's bearer, so the token never reaches the browser.
app/api/conversations/[id]/events/route.ts
import { bffStream } from "@/lib/bff-stream";export const dynamic = "force-dynamic";export const GET = bffStream({ upstream: (req) => `${process.env.API_URL}/conversations/${conversationId(req)}/events`, getAccessToken: async () => (await auth())?.accessToken ?? null,});Installation
Once per app, set up Aedis UI. The CLI verifies the signed release before writing anything and pins every dependency:
npx @aedis/cli@0.1.0 initImport the theme once in your global CSS, after Tailwind:
app/globals.css
@import "tailwindcss";@import "./styles/aedis-theme.css";Then add the component:
npx @aedis/cli@0.1.0 add bff-stream# pnpm dlx @aedis/cli@0.1.0 add bff-stream · yarn dlx @aedis/cli@0.1.0 add bff-stream · bunx @aedis/cli@0.1.0 add bff-streamProps
| Prop | Type | Default | Description |
|---|---|---|---|
| upstream | string | URL | (request) => string | URL | — | The API stream, from configuration; http(s) only. Never read a URL from the request. |
| getAccessToken | (request) => string | null | — | The bearer of the server-side session; null answers 401 without calling the API. |
| headers | Record<string, string> | — | Extra headers for the API; credentials, cookies and hop-by-hop headers throw. |
| log | (message, details) => void | console.error | Where failure details go. |
| connectTimeoutMs | number | 15000 | Wait for the API to start the stream. |
Security
- The bearer comes from the server session only; the browser never sees it.
- Nothing from the browser goes upstream except a Last-Event-ID of at most 256 printable characters: no cookies, no Authorization, no query.
- No-store, no-transform and X-Accel-Buffering: no; upstream headers (Set-Cookie, internal ones) are not copied.
- The API request is aborted when the browser disconnects, and after connectTimeoutMs without a response.
- The browser gets a bare status (401, 403, 204 or 502); details go to the server log. server-only keeps it out of client bundles.