Frontend (BFF)¶
The frontend is a Next.js 15 (App Router) app built on the
Backend-for-Frontend pattern. The browser only calls same-origin
/api/* route handlers, which proxy to the Go API from the server.
Why a BFF?¶
sequenceDiagram
participant B as Browser
participant N as Next.js route handler
participant A as Go API
B->>N: fetch('/api/gov/applications', {headers: x-dgov-csrf})
Note over N: reads the token from an httpOnly cookie<br/>checks the origin
N->>A: GET /api/v1/gov/applications<br/>Authorization: Bearer …
A-->>N: {data: …}
N-->>B: {data: …} (the token NEVER travels back)
Result: access and refresh tokens live in httpOnly cookies that client
JavaScript cannot read. Even with an XSS, the tokens do not leak directly.
Double CSRF protection¶
Every mutating browser call goes through sendJSON / postJSON in
lib/client.ts, which add the x-dgov-csrf header. On the BFF side,
checkOrigin in lib/bff.ts requires both:
- The custom header — a cross-site form submission cannot set it.
- Origin / Referer matching our own origin.
When writing a new mutating route
New POST/PUT/DELETE BFF routes must call checkOrigin first.
Forget it and CSRF is wide open.
Tokens and session¶
| File | Role |
|---|---|
lib/cookies.ts |
Reading/writing httpOnly cookies |
lib/session.ts |
Session state and expiry |
lib/api.ts |
Server-side fetch and tryRefresh |
lib/signout.ts |
Logout — backend call plus cookie cleanup |
Never call refresh inside an RSC
Refresh rotates the token. Called from a context that cannot write
cookies, the new token is lost and the old one is already invalid — the user
is stuck. tryRefresh therefore probes cookie writability first
(lib/api.ts).
Data fetching¶
Component data goes through TanStack Query:
const { data } = useQuery({
queryKey: ['gov', 'applications'],
queryFn: () => getJSON('/api/gov/applications'),
});
// after a mutation:
queryClient.invalidateQueries({ queryKey: ['gov', 'applications'] });
The provider lives in components/Providers.tsx. Dependencies are kept minimal —
no UI framework, just lucide-react (icons) and qrcode.react (the eID QR).
Internationalisation¶
UI strings are centralised in lib/i18n.ts in four languages: mn · en ·
zh · ru, consumed through useT().
lib/i18n.test.ts enforces key completeness — a key missing in any language
fails the test. prefersLatinName() prefers the Latin name in non-Cyrillic
languages (Russian is Cyrillic, so it keeps the Mongolian spelling).
The AI answer language
Every chat request carries the UI language in a lang field, and that is
the decider — even if the user writes in another language, the answer comes
back in the UI language. See AI assistant.
Theming¶
Site appearance is configured by an admin (accent colour, font, density, light/dark), with a per-user override:
| Surface | API |
|---|---|
| Public default | GET /api/site/appearance |
| Admin change | PUT /api/admin/site/appearance (settings.manage) |
| Named themes | /api/admin/themes/* — CRUD and activation |
Brand colours are never hardcoded — they come from Admin → Theme editor.
BFF route layout¶
There are 150+ route handlers under src/app/api/, grouped to mirror the
backend modules:
api/auth/* eID sign-in, SSO, super-admin onboarding
api/me/* profile, eID PKI console, signature
api/gov/* government services and officer queue
api/registry/* service registry
api/relay/* request relay
api/gateway/* API gateway
api/applications/* OAuth client registry
api/ai/* chat · STT · TTS · translation
api/public/ai/* anonymous chat (streaming)
api/integrations/* Google Drive/Meet · Dropbox
api/gspace/* SFTP storage
api/provider/* OIDC login / consent / logout
api/audit, api/security, api/site, api/themes, api/superadmin …
Backend errors are relayed through proxyResult / toClientResponse — tokens
and internal messages never leak.
Tests¶
| Test | What it proves |
|---|---|
i18n.test.ts |
Dictionary and landing-copy parity across four languages |
bff.test.ts |
checkOrigin, proxied response shape |
securityHeaders.test.ts |
CSP and related header values |
chatStream.test.ts |
SSE chat stream parsing |
navigation.test.ts |
Permission-driven menu structure |