3.4 KiB
The BFF (Backend-For-Frontend)
The UI never talks to a real backend. It calls its own BFF — Next.js Route Handlers under
apps/web/app/api/bff/**. This is the seam that keeps the frontend independent of whatever
backend you eventually run (IIOS service, a NestJS BFF, Supabase, …).
Why a BFF
- One shaped API per frontend. Endpoints return exactly what the UI needs, nothing more.
- Secrets stay server-side. Tokens/keys for the real backend live in the route handlers, never in the browser bundle.
- Backend independence. Change the handlers; the SDK + components don't move.
Endpoint contract
Base path: /api/bff. All JSON. (These mirror the BffClient methods in the SDK.)
| Method | Path | Body / Query | Returns |
|---|---|---|---|
| GET | /bootstrap |
— | { me, workspace, workspaces, users, sections, channels } |
| GET | /channels |
— | { channels } |
| GET | /channels/:id/messages |
— | { messages } |
| POST | /channels/:id/messages |
{ body, parentId?, scheduledFor? } |
{ message } (201) |
| GET | /channels/:id/threads/:parentId |
— | { parent, replies } |
| POST | /channels/:id/messages/:mid/reactions |
{ emoji } |
{ message } |
| POST | /channels/:id/messages/:mid/pin |
— | { message } |
| POST | /channels/:id/messages/:mid/save |
— | { message } |
| GET | /inbox |
?state=OPEN|SNOOZED|DONE|ARCHIVED |
{ items } |
| PATCH | /inbox/:id |
{ state } |
{ item } |
| GET | /support/tickets |
?scope=mine|assigned|all |
{ tickets } |
| GET | /support/tickets/:id |
— | { ticket } |
| PATCH | /support/tickets/:id |
{ state } |
{ ticket } |
| POST | /support/tickets/:id/reply |
{ body } |
{ ticket } |
| GET | /support/availability |
— | { agents } |
| GET | /search |
?q= |
{ results } |
| GET | /notifications |
— | { notifications } |
The mock store
apps/web/lib/mock/store.ts is a module singleton seeded from data.ts. It persists across
requests in one Node process (resets on restart). Each handler is a thin adapter:
// app/api/bff/channels/[id]/messages/route.ts
export async function POST(req, { params }) {
const { id } = await params;
const { body, parentId, scheduledFor } = await req.json();
const message = store.send(id, body, { parentId, scheduledFor });
return NextResponse.json({ message }, { status: 201 });
}
Swapping in a real backend
Two options:
A. Keep the BFF inside Next (recommended). Replace the store.* calls with fetch() to
your backend inside each handler. Add auth there. Example against IIOS:
// app/api/bff/channels/[id]/messages/route.ts
export async function GET(_req, { params }) {
const { id } = await params;
const token = await getIiosToken(); // server-side only
const res = await fetch(`${process.env.IIOS_URL}/v1/threads/${id}/messages`, {
headers: { authorization: `Bearer ${token}` },
});
const data = await res.json();
return NextResponse.json({ messages: data.messages.map(toSdkMessage) }); // map shapes
}
B. Standalone NestJS BFF. Point the SDK at it with NEXT_PUBLIC_BFF_BASE_URL=https://bff…
and implement the same 17 endpoints in NestJS. The BffClient prefixes /api/bff, so mount
your Nest routes under that prefix (or adjust client.ts).
Either way the SDK and every component are unchanged. See
IIOS_INTEGRATION.md for the full mapping.