ADR-004: Backend-for-Frontend (BFF) Pattern

On this page

Status

Accepted

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() with CRAIG_WEB__SESSION_SECRET as 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 of tower-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_auth middleware 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.

Edit this page · latest