Decision. Build the SPA now. Do not build the BFF yet — but build the SPA so that adding one is a 2–5 day change, and treat that discipline as non-negotiable rather than aspirational. The retrofit is a two-way door precisely because the API never changes: a BFF forwards a bearer token to it, so
AddJwtBearerstays exactly as it is [1][2].
The two arguments that actually decide this
The “it’s internal, the BCP isn’t about us” defence fails. The draft’s strongest sentence names the case to its face: the BFF architecture is “strongly recommended for business applications, sensitive applications, and applications that handle personal data” [3]. Its threat model assumes only that attacker code runs in your origin — nothing about public users or consumer scale. But the same document prices its own advice honestly, presenting the patterns as “a different trade-off between security and simplicity” in decreasing order of security [4], and attaches no RFC 2119 keyword. It is at revision -27, IESG-approved, in the RFC Editor queue. So: a strong prior, not a mandate.
The security win is real but bounded, and smaller than the marketing. A BFF does not stop XSS; it converts silent, off-origin, outlives-the-tab token exfiltration into loud, tab-bound session riding through your own logs, where you can see it and kill the session [5]. For an admin console that distinction matters less than elsewhere — if the payload can grant itself a role in the first 400 ms, the tab-lifetime bound bought you nothing. Auth0’s framing is the honest one: “BFF shifts the security boundary. It doesn’t eliminate it” [6].
Which leaves supply chain as the one pro-BFF argument that survives all six angles. Strict CSP plus Trusted Types — now Baseline, with Firefox shipping in Feb 2026 [7] — genuinely closes injected-script XSS, and allowlist CSP without them is near-worthless (Google measured 94.68% of script-limiting policies as ineffective) [8]. But CSP cannot distinguish your trojaned npm dependency from your own code: the nonced bundle executes with full privilege. Against a September-2025-style compromise, “there is no token in the browser to steal” is doing work that no CSP can do [5].
What the angles say to each other
A DNS choice, not a security choice, moves the needle most. Iframe silent renew is not dead — it is dead cross-site. auth.itenium.be and admin.itenium.be are different origins but the same site, so the IdP cookie is first-party and prompt=none still works in 2026 [9][10]. Put Keycloak on a subdomain of the app’s registrable domain and the pure-SPA path stays cheap; put it on a foreign host and the SPA’s renewal story degrades into full-page redirects, which is where the BFF starts winning on ergonomics rather than on security.
The costs are not where people expect. Codegen is a non-issue: a path-preserving proxy leaves the OpenAPI document and the generated client untouched — only baseUrl and credentials: 'include' change. Only a composing BFF turns codegen into a project, and Duende’s default MapRemoteBffApiEndpoint shape is what quietly creates that problem [11]. The genuine operational tax is the session store and a shared Data Protection key ring once you run more than one replica [12] — and you pay that whenever you adopt, so it is not an asymmetry at all. Which means the honest cost comparison is not “BFF now vs BFF later”; it is “BFF ever vs never”.
The one unbounded retrofit is real-time. SignalR and WebSockets through a BFF are the single genuinely painful item: a WebSocket handshake cannot set the anti-forgery header the BFF requires, and teams report falling back to long-polling [13]. If itenium-ui will carry live updates, decide before you build — that is the one place where “later” is expensive.
If and when you build it
Duende.BFF is the reference implementation, works against any OIDC provider including Keycloak, and is ~50 lines of config — but production use needs a licence, listed at $5,750/yr for Lite, free only under the Community Edition’s <$1M revenue threshold; “it’s just an internal tool” does not qualify you, because the gate is org revenue, not app audience [14][15]. The free path is YARP + AddOpenIdConnect + Duende.AccessTokenManagement (Apache-2.0) — which is Microsoft’s own documented recipe, at the price of ~250 lines of security-critical code you own forever [16]. Note that you are not choosing between Duende and YARP: Duende.BFF’s forwarder is YARP. You are choosing whether to buy the auth layer on top of it. .NET 10 narrowed the gap — the cookie handler now returns 401 instead of a login redirect for API endpoints [17] — but first-party OAuth refresh-token support is still unshipped and has slipped to .NET 11 planning [18].
What would change the call
Revisit the moment any of these become true: the console needs real-time; it starts handling customer PII or becomes reachable from outside the corporate perimeter; a second client (mobile, native, a partner integration) appears and wants bearer tokens anyway; or a supply-chain incident lands close enough to make “nothing in the browser to steal” worth $5,750 a year. Absent those, the marginal security a BFF buys an internal console is the tail of the risk — offline replay and refresh-token abuse — not the body of it, and the body is what a hardened CSP and Trusted Types actually address.
The sharpest open question is not whether the BFF is worth it, but whether the discipline it depends on will hold: the entire 2–5 day retrofit estimate rests on nobody ever calling jwtDecode() in a component or reaching for getAccessToken() from app state. Every team that missed that estimate missed it there.