Skip to content

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:

  1. The custom header — a cross-site form submission cannot set it.
  2. 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().

export const LANGS = ['mn', 'en', 'zh', 'ru'] as const;

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

npm run test      # vitest
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