ADR-004: Backend-for-Frontend (BFF) Pattern
On this page
Context
CRAIG needs a web interface for caseworkers, supervisors, and administrators. Options considered: client-side SPA (React/Vue/Angular), server-side rendered with API proxy (BFF), or a combination.
Decision
Use a server-side rendered BFF (craig-web) built with:
-
Axum — HTTP framework (consistent with backend services)
-
Askama — compile-time template engine (Jinja2-like syntax)
-
htmx — server-driven interactivity without full page reloads
-
Alpine.js — lightweight client-side state for forms and toggles
This follows the pattern used by OpenStack Horizon (Django-based BFF for OpenStack APIs).
Rationale
-
Security: the browser never sees the JWT directly. The token is wrapped in an AES-GCM encrypted cookie via
tower-cookies::Cookies::private()withCRAIG_WEB__SESSION_SECRETas the master key — the browser holds only ciphertext, so XSS cannot extract a usable bearer token. See ADR-013 for the storage rationale (stateless encrypted cookie instead oftower-sessions::MemoryStore) and the in-progress mobile client work for the one place this BFF assumption might need to relax. -
Simplicity: No JavaScript build step, no Node.js dependency in the service itself, no client-side routing. Templates compile at Rust build time via Askama.
-
Performance: Server-side rendering produces complete HTML on first load. htmx provides partial page updates for interactive features without the complexity of a virtual DOM.
-
OIDC integration: The BFF handles the full Authorization Code + PKCE flow with Keycloak. The browser redirects to Keycloak, receives a code, and the BFF exchanges it for tokens server-side.
-
Consistency: The BFF is an Axum service like the others — same deployment model, same Rust toolchain, same Docker build pipeline.
-
Accessibility: Server-rendered HTML with progressive enhancement (htmx) provides excellent baseline accessibility compared to JavaScript-heavy SPAs.
RBAC Model
The BFF does not enforce route-level authorization:
-
require_authmiddleware checks authentication only (valid session exists) -
Admin-only actions are hidden in templates via
ctx.user.is_admin() -
Authorization is delegated to backend APIs via Bearer JWT — if a user somehow reaches a restricted action, the API returns 403
This avoids duplicating authorization logic between the BFF and APIs.
Consequences
-
No offline capability or client-side caching — every page requires a server round-trip.
-
Interactive features are limited to what htmx + Alpine.js can provide. Complex dashboards or real-time features may require additional JavaScript.
-
The BFF must be deployed alongside the API services — it proxies all requests.