Build CoreOrdered learning track

SPA vs SSR vs BFF Auth

Learn React Authentication, Authorization, Identity & Permission/ACL - Part 071

SPA vs SSR vs BFF authentication architecture for React apps: trust boundaries, token exposure, session model, data loading, CSRF/XSS trade-offs, deployment constraints, and decision matrix.

9 min read1605 words
PrevNext
Lesson 71130 lesson track25–71 Build Core
#react#authentication#authorization#spa+8 more

Part 071 — SPA vs SSR vs BFF Auth

A React authentication architecture is not chosen by asking:

Where should I put the login button?

It is chosen by asking:

Where is the security boundary?
Where are tokens stored?
Who can call the resource server?
Where is authorization enforced?
Where does session state live?
What happens when the browser is compromised?
What happens when the user opens three tabs?
What happens when the IdP is down?

The answer determines whether your app behaves like:

1. Pure SPA
2. SSR application
3. Backend-for-Frontend application
4. Token-mediating backend
5. Hybrid architecture

This part builds the decision framework.

We are not choosing a fashionable stack. We are choosing a trust boundary.


1. The core problem

A React app runs in a hostile environment.

The browser can be affected by:

XSS
malicious extensions
supply-chain compromise
shared computers
back/forward cache
service workers
local persistence
third-party script compromise
user-driven copy/paste leakage
debug tooling
browser plugin inspection

If the browser holds long-lived credentials, then browser compromise becomes credential compromise.

So the architecture question becomes:

How much credential authority do we allow JavaScript to possess?

That question divides the architectures.


2. The four common shapes

2.1 Pure SPA

Browser React app owns OAuth flow and calls APIs directly.

Characteristics:

JavaScript handles OAuth transaction.
JavaScript may hold access token.
API is called directly from browser.
CORS is part of the security surface.
Token refresh must be coordinated in browser.
XSS impact can be high if token is available to JS.

Pure SPA can be valid for lower-risk systems, internal tools, or architectures that use short-lived in-memory access tokens and no browser-exposed refresh token.

But the failure mode is clear:

If JS is compromised, token-bearing API access may be compromised.

2.2 SSR app

Server participates in rendering and can read cookies/request headers before React renders.

Characteristics:

Server can gate route before render.
Server can fetch sensitive data without exposing API tokens to JS.
Cookies become natural session transport.
Hydration mismatch must be handled.
Cached SSR output must be carefully controlled.
Client-side navigation still needs auth-aware behavior.

SSR is not automatically secure.

SSR can still leak data if:

server-rendered HTML is cached incorrectly
RSC payload includes sensitive data in wrong boundary
client bundle includes secrets
server action lacks authorization
middleware only checks route, not resource action

2.3 Backend-for-Frontend

A backend specifically built for the frontend owns OAuth tokens and exposes app-shaped endpoints to the React app.

Characteristics:

Browser holds session cookie, not OAuth tokens.
BFF holds access/refresh tokens server-side.
BFF adapts APIs for frontend needs.
BFF can centralize CSRF, cache-control, audit, tenant context, and authorization checks.
CORS can be minimized by same-origin deployment.

BFF is often the strongest default for high-risk React apps.

Its cost:

more backend code
more deployment responsibility
more server capacity
more operational surface
potential backend bottleneck
careful CSRF design required

2.4 Token-mediating backend

Backend mediates token issuance/storage but browser may still call APIs directly or obtain limited tokens.

This is between pure SPA and full BFF.

This can reduce refresh token exposure while preserving some direct browser-to-API behavior.

But it is harder to reason about because authority is split.

Use it only when you have a clear reason.


3. The axis that matters

Do not compare these architectures as labels.

Compare them by axes.

AxisPure SPASSRBFFToken-mediating backend
OAuth tokens visible to JSOften yesUsually noNoSometimes limited access token
Browser session transportMemory/token/cookieCookieCookieCookie + limited token
API called from browserYesSometimesUsually noSometimes
CORS complexityHighMediumLow if same-originMedium
CSRF riskDepends on cookiesYes if cookie-basedYes if cookie-basedYes if cookie-based
XSS token theft impactHigher if token in JSLower for server-held tokensLower for server-held tokensMedium
Server complexityLowerMediumHigherHigher
Data shapingClient-sideServer/clientBFF-ownedMixed
SSR compatibilityNoYesYes/optionalYes/optional
Authorization localityAPI only or mixedServer/APIBFF/APIMixed
Operational visibilityFragmentedBetterStrongerStronger but split

The safest architecture is not always the simplest.

The simplest architecture is not always safe enough.


4. Threat model comparison

4.1 XSS

If an attacker executes JavaScript in your origin, they may be able to:

read localStorage/sessionStorage/IndexedDB
read in-memory JS variables indirectly through app behavior
make authenticated requests from the page
capture UI data
modify DOM
abuse API clients
exfiltrate non-HttpOnly data
trigger state-changing actions

Architecture changes the blast radius.

ArchitectureXSS can steal token?XSS can perform actions?Notes
Pure SPA with JS tokenOften yesYesWorst if refresh token is exposed.
Pure SPA with in-memory access tokenMaybe during runtimeYesLess persistence, still runtime risk.
Cookie SSR/BFFUsually no OAuth token theftYesXSS can still send same-origin requests.
BFF + CSRF + action confirmationNo OAuth token theftSome actions, constrained by server policySensitive actions need step-up/re-auth.

Important:

HttpOnly cookies reduce token theft.
They do not make XSS harmless.

XSS can still ride the user session unless server-side controls exist.


4.2 CSRF

CSRF matters when authentication is automatically attached by the browser.

Cookie-based auth => CSRF must be designed.
Bearer token in Authorization header => classic CSRF is less direct, but XSS/token theft becomes more central.

BFF/SSR cookie architectures must treat CSRF as first-class:

SameSite cookies
CSRF tokens for unsafe methods
Origin/Referer validation
content-type restrictions
anti-replay/idempotency keys
step-up for sensitive actions

The choice is not:

cookies are secure, tokens are insecure

The real choice is:

Which attack class are we optimizing against, and what controls do we have?

4.3 Token replay

Bearer tokens are usable by whoever possesses them.

Pure SPA direct API architecture must assume:

access token leak = API access until expiry/revocation
refresh token leak = long-lived compromise unless rotation/reuse detection works

BFF reduces this by keeping tokens server-side.

But BFF session cookies can still be abused if stolen or fixed, so server-side session management remains required:

session id entropy
rotation after login
server-side expiry
revocation
device/session inventory
suspicious reuse detection
secure cookie attributes

5. Session authority models

There are three common ways to represent user continuity.

5.1 Browser-held bearer authority

Browser holds token that API accepts.
await fetch('https://api.example.com/cases/123', {
  headers: {
    Authorization: `Bearer ${accessToken}`,
  },
});

Pros:

simple API integration
works with external APIs
scales without app server session lookup
natural for mobile/native parity

Cons:

credential accessible to JS unless carefully avoided
browser refresh coordination is hard
revocation often delayed
CORS complexity
API response shapes leak directly to frontend

5.2 Server-held session authority

Browser holds opaque session cookie.
Server maps session to identity/token state.
Cookie: __Host-app_session=sid_opaque_random_value

Pros:

OAuth tokens not visible to JS
server-side revocation is easier
same-origin deployment can avoid broad CORS
server can centralize cache-control and audit

Cons:

requires session storage
requires CSRF controls
server is in hot path
horizontal scaling/session replication concerns

5.3 Stateless signed session

Cookie contains signed/encrypted session claims.

Pros:

less server lookup
simple horizontal scaling

Cons:

revocation is harder
claim freshness problem
cookie size limits
rotation complexity
risk of treating stale claims as authority

For high-risk authorization, prefer server-side session reference or server-side permission checks.

A signed cookie may be acceptable for low-risk projection, but do not stuff dynamic permissions into it and call it done.


6. Data loading differences

6.1 Pure SPA data loading

Risk:

component may render shell before auth known
stale query cache can leak data
client-only guard can be bypassed by direct API call

Mitigation:

loader-level auth if using React Router Data/Framework mode
query key includes auth/tenant epoch
cache clear on logout
API validates every request

6.2 SSR data loading

Risk:

server response cached incorrectly
hydration mismatch after session changes
server/client auth logic drift
sensitive data serialized into client payload accidentally

Mitigation:

Cache-Control: no-store for sensitive pages/data
central auth helpers
server-only modules for token handling
typed session projection
hydration-safe auth state

6.3 BFF data loading

Risk:

BFF becomes accidental superuser
BFF endpoint lacks resource authorization
BFF over-fetches sensitive downstream data
BFF caches user data globally

Mitigation:

BFF calls APIs with least-privilege token/context
BFF checks subject/action/resource/context
BFF returns UI-shaped minimum data
BFF logs actor/session/tenant/resource decision

7. Authorization placement

A robust system can have multiple authorization layers.

But do not confuse layers.

LayerPurposeIs it security enforcement?
UI permission gateHide/disable/explain actionsNo, exposure control only
Router loader/action guardPrevent wrong navigation/data loadingPartial, only for app routes
BFF checkApp boundary authorizationYes, but not enough if downstream APIs callable elsewhere
API checkResource/action enforcementYes
Policy engineDecision authorityYes, if integrated correctly
DB/RLS/object guardLast-line isolationYes, for supported domains

The API/resource server should remain authoritative for protected resources.

BFF can enforce app-specific decisions, but it should not be the only guard if the downstream API can be accessed by other clients.


8. Same-origin vs cross-origin

8.1 Same-origin BFF

https://app.example.com
  /app/*       -> React app
  /api/*       -> BFF endpoints
  /auth/*      -> login/callback/logout

Benefits:

cookie scoping is simpler
CORS can be avoided
CSRF can be standardized
frontend deployment and BFF share origin
security headers can be centralized

Trade-offs:

frontend/backend deployments coupled
edge/CDN routing must be precise
cache-control mistakes can affect both

8.2 Cross-origin SPA + API

https://app.example.com
https://api.example.com

Benefits:

independent deployment
API can serve multiple clients
clear service boundary

Trade-offs:

CORS preflight complexity
cookie domain/SameSite complexity if using cookies
more leakage surface in browser network layer
more client-side token handling

9. Architecture decision matrix

Use this as a first-pass decision tool.

RequirementPreferred direction
High-risk enterprise SaaSBFF or SSR+BFF
Regulated case managementBFF + server-side policy + audit
Public content with light account featuresSSR or hybrid
Offline-first appSPA with careful token/session constraints
External API must be called directly from browserPure SPA or token-mediating backend
Avoid exposing refresh token to browserBFF/token-mediating backend
Need strong server-rendered access checksSSR/BFF
Need minimal backend footprintPure SPA
Need centralized audit/correlationBFF/SSR
Need multi-tenant SSO with enterprise controlsBFF/SSR+BFF
Need multiple frontend clients sharing APIAPI-enforced auth + optional BFF per frontend
Need rich UI data shapingBFF

Decision rule:

The more valuable the data and the more complex the authorization,
the less authority should live in browser JavaScript.

10. High-risk domains should bias toward BFF

In systems like:

regulated case management
enforcement lifecycle platforms
financial approval workflows
healthcare admin systems
identity/admin portals
enterprise SaaS admin consoles
legal document platforms
high-value collaboration tools

BFF is often worth the cost.

Why?

Because these domains usually need:

object-level authorization
field-level permission
step-up for sensitive actions
strong audit trail
session revocation
multi-tenant SSO
impersonation controls
safe export/download
server-side data minimization
policy explanation
incident response capability

Those are easier to standardize at a BFF boundary than across hundreds of React components.


11. But BFF is not magic

A BFF can become insecure if it is treated as a trusted tunnel.

Bad BFF:

/browser asks BFF for /proxy?url=/admin/users/123
BFF forwards request with powerful service token
API trusts BFF blindly
BFF returns everything to browser

This creates a confused deputy.

Good BFF:

/browser asks BFF for /cases/123/summary
BFF validates session
BFF derives actor/tenant
BFF checks can(actor, 'case.read.summary', case:123)
BFF calls downstream with actor context or constrained token
BFF shapes minimum response
BFF records audit decision

The BFF should reduce browser authority, not concentrate unlimited backend authority.


12. SSR-specific risks

SSR and RSC-style architectures add a different set of auth bugs.

12.1 Cache leakage

If an authenticated server-rendered response is cached as public, user data can leak.

Bad:

HTTP/1.1 200 OK
Cache-Control: public, max-age=3600

For sensitive authenticated responses:

Cache-Control: no-store
Vary: Cookie

Use cache intentionally.

Never let framework defaults decide for protected data.


12.2 Server/client auth mismatch

Server sees session as valid.

Client hydrates later after logout in another tab.

Now the app has:

server-rendered protected data
client state says anonymous

Handle explicitly:

clear client caches
redirect or show logged-out overlay
avoid keeping sensitive UI interactive
broadcast logout across tabs

12.3 Secret leakage into client bundle

Server code and client code must be separated.

Bad:

// imported by a client component by accident
export const clientSecret = process.env.OAUTH_CLIENT_SECRET;

Good:

server-only token utilities
lint/import rules
framework server-only markers
build-time checks
no secret in public env variables

13. Pure SPA can still be disciplined

A pure SPA does not have to be reckless.

Minimum discipline:

Authorization Code with PKCE
no implicit flow
no long-lived refresh token in localStorage
prefer in-memory access token
short access token lifetime
refresh rotation if refresh exists
reuse detection server-side
strong CSP
no third-party script sprawl
API validates every request
query cache cleared on logout/tenant switch
multi-tab logout coordination

But the architecture still has a limit:

The browser remains the token-bearing client.

That may be acceptable.

It may not be acceptable.

Decide explicitly.


14. SSR + BFF hybrid

Many production React systems are not one shape.

A strong hybrid:

SSR for initial protected page/data
BFF for UI-shaped API endpoints
same-origin cookies
server-side token handling
React client for interactivity
resource APIs still enforce authorization

This can provide:

fast initial auth decision
reduced token exposure
central session management
clear API for client interactions
better audit trail

But it requires consistent shared auth primitives.

Do not implement separate auth logic in SSR loaders, server actions, API routes, and BFF handlers.

Extract one policy boundary.


Tier 1 — Simple product, low-risk data

Pure SPA + Authorization Code PKCE
short-lived in-memory access token
no localStorage refresh token
API validates every request

Use when:

low sensitivity
small team
simple permissions
no enterprise SSO complexity

Tier 2 — Product SaaS, medium-risk data

SSR or BFF
HttpOnly cookie session
server-side token storage
resource API authorization
permission projection endpoint

Use when:

multi-tenant users
moderate object permissions
exports/downloads
customer admin UI

Tier 3 — Regulated/high-risk workflow

SSR+BFF
server-side sessions
central policy engine
object/workflow/field authorization
audit events
step-up auth
impersonation controls
strict cache-control
incident runbooks

Use when:

case management
enforcement lifecycle
financial approval
sensitive documents
high-impact mutations

16. Architecture smell catalog

Smell: role check in route component

if (user.role === 'admin') return <AdminPage />;

Problem:

role is stale
resource/action/context ignored
API may still allow direct access
UI and server drift

Better:

await requirePermission(request, {
  action: 'admin.user.read',
  resource: { type: 'tenant', id: tenantId },
});

Smell: JWT in localStorage because it is easy

Problem:

persistent JS-readable credential
high XSS blast radius
no server-side session inventory
hard forced logout

Better:

BFF session cookie
or in-memory access token with short lifetime
or token-mediating backend if direct API calls are unavoidable

Smell: BFF proxy without authorization

app.all('/api/proxy/*', async (req, res) => {
  return forward(req, internalApi);
});

Problem:

BFF becomes confused deputy
frontend can access arbitrary backend routes
no app-level policy

Better:

explicit app endpoints
explicit actions
resource-level authorization
minimum response shape

Smell: SSR protected data with public cache

Problem:

shared cache or CDN may leak user-specific response
back button may reveal sensitive page after logout

Better:

Cache-Control: no-store
Vary: Cookie

for sensitive authenticated responses.


17. Decision checklist

Before choosing architecture, answer these.

Identity and session

Who authenticates the user?
Where is the session stored?
Can the user revoke a session?
Can admins force logout?
How are multiple tabs coordinated?
How are refresh failures handled?

Token exposure

Does browser JavaScript ever see access tokens?
Does browser JavaScript ever see refresh tokens?
Are tokens persisted?
Are tokens audience-restricted?
How are token leaks detected?

Authorization

Where is object-level authorization enforced?
Where is workflow-state authorization enforced?
Where are field-level permissions enforced?
How does UI learn allowed actions?
How are permission changes invalidated?

Browser security

What is the XSS blast radius?
What is the CSRF strategy?
What is the CSP strategy?
Are third-party scripts allowed on authenticated pages?
Can service workers cache protected responses?

Operations

Can we audit login/logout/access denied/mutation?
Can we debug 401/403 mismatch?
Can we rotate secrets?
Can we revoke sessions?
Can we survive IdP downtime?
Can we run incident response after token leak?

18. A practical recommendation

For advanced React engineers, the default recommendation is:

Do not start from protected routes.
Start from token exposure and enforcement boundary.

Then:

If data is low-risk and backend capacity is limited:
  SPA + PKCE + short-lived tokens + strict API authorization.

If data is medium/high-risk:
  BFF or SSR+BFF with HttpOnly cookie session and server-held tokens.

If permissions are object/workflow/field-level:
  central policy engine or server-side permission service.

If the app is regulated:
  BFF + audit + step-up + cache-control + incident runbooks.

19. Minimal architecture templates

Pure SPA template

React SPA
  Auth client: PKCE, in-memory access token
  Query client: auth/tenant scoped cache
  API client: Authorization header
  API: validates every request
  Permission API: returns allowed actions
  Security: CSP, no localStorage token, refresh rotation

SSR template

React framework server
  Session cookie: HttpOnly Secure SameSite
  Server loader: validate session
  Server fetch: API call with server credentials or user token
  Cache: no-store for sensitive data
  Client hydration: projected session only

BFF template

React app
  Browser holds app session cookie only
BFF
  owns OAuth callback
  stores tokens server-side
  exposes app-specific endpoints
  checks authorization
  calls downstream APIs
  audits decisions
Resource APIs
  still enforce resource authorization

20. Part conclusion

SPA, SSR, and BFF are not just rendering choices.

They are security boundary choices.

The core invariant:

The more sensitive the system and the more complex the authorization, the less long-lived authority should live in browser JavaScript.

A top-tier engineer does not ask:

Can I make login work?

They ask:

What authority exists in each runtime?
How can it be abused?
Where is it validated?
How is it revoked?
How is it audited?
How does it fail?

That is the difference between a login implementation and an auth architecture.


References

  • Next.js Docs — Authentication Guide: authentication, session management, and authorization concepts.
  • OAuth 2.0 for Browser-Based Applications — browser app architecture patterns including BFF/token-mediating backend.
  • RFC 9700 — Best Current Practice for OAuth 2.0 Security.
  • OWASP Session Management Cheat Sheet.
  • OWASP Authorization Cheat Sheet.
  • OWASP CSRF Prevention Cheat Sheet.
  • OWASP Web Security Testing Guide — Testing for Browser Cache Weaknesses.
Lesson Recap

You just completed lesson 71 in build core. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.