Skip to content
5 min read Moustakime KIFIA

The BFF that swaps an API JWT for an httpOnly cookie

Why a well-placed BFF can act both as a safer web-session boundary and as a data-shaping layer for the frontend.

  • Auth
  • Engineering
  • Integrations

This is not really an article about setting a cookie. It is an article about where the real security boundary should live in a web app that talks to a BFF.

When the browser calls a backend directly with a token it handles itself, auth tends to leak into too many places: client code, local storage, network helpers, UI guards and eventually test setup.

The pattern that holds up better here is simpler to maintain: the browser talks to server-side Next.js routes, those routes talk to the BFF, and the API token is turned back into an httpOnly cookie managed on the server side.

The problem to solve

The problem is not only logging a user in. The problem is preserving all of this at once:

  • relatively simple frontends
  • tokens kept out of application-side JavaScript
  • a clear integration layer between the web app, the BFF and the business API
  • one consistent way to rebuild the session on /api/* routes

Without that boundary, complexity comes back quickly:

  • token storage in localStorage or browser memory
  • direct browser-to-BFF calls
  • repeated auth logic across pages and hooks
  • a larger XSS surface to reason about

Chosen solution

The chosen shape is to make server-side Next.js routes own login and proxying.

The flow stays intentionally small:

  1. the browser posts credentials to a Next.js route
  2. that route calls the BFF
  3. the BFF talks to the business API
  4. the Next.js route writes an httpOnly cookie
  5. the rest of /api/* reads that cookie and proxies to the BFF

The important point is not the cookie by itself. The important point is that the browser no longer has to carry the auth contract with the API on its own.

Why this works well in this kind of platform

This pattern works because it creates a clean boundary:

  • the browser owns UI and forms
  • Next.js routes own the web session
  • the BFF owns frontend-to-API adaptation
  • the business API stays behind an already authenticated layer

But that boundary alone is not enough to justify a BFF.

The second important role is shaping data for the frontend.

In a multi-service environment, the frontend should not have to know:

  • response format differences across APIs
  • naming conventions that belong to each service
  • the multiple calls required to build one useful view

That is where the BFF can:

  • harmonise contracts
  • restructure a response for actual UI needs
  • aggregate several services
  • enrich data before returning it

In other words, the frontend no longer consumes several raw services. It consumes an interface that is already adapted to its use case.

A BFF is only worth it when it absorbs a real contract or composition difference. Here it does: it absorbs the difference between browser code, Next.js routes and the business API, but also part of the adaptation work the frontend should not own.

How we implement it

The first useful anchor is the Platform login route:

const response = NextResponse.json({ status: "ok" }, { status });

response.cookies.set("authToken", data.token, {
  httpOnly: true,
  sameSite: "strict",
  secure: process.env.NODE_ENV === "production",
  path: "/",
  maxAge: 60 * 60 * 24 * 7
});

The point is not to show a lot of code. The point is to make the responsibility visible:

  • the route receives credentials
  • it calls the BFF via fetchFromBff("/auth/login")
  • it validates the response
  • it turns the JWT into a web cookie

The second useful anchor is the passwordless variant in apps/member/src/app/api/auth/login-with-code/route.ts.

It repeats the same architectural move:

  • server-side call to the BFF
  • read token or access_token
  • write the authToken cookie

That variant matters because it shows the pattern is not tied to email/password. It also works for other entry flows as long as the boundary stays the same.

What this removes from the codebase

The most important gain is often negative: what we no longer have to write.

This pattern removes:

  • direct backend calls from client code
  • UI branches that explicitly manipulate the JWT
  • part of the risk introduced by browser-side token storage
  • repeated auth contracts across frontend API clients
  • part of the light orchestration work that would otherwise leak into the frontend

It also makes the session model easier to read. A route under /api/* can read request.cookies.get("authToken") and rebuild a clean Authorization header for the BFF.

And when the BFF already composes the right response, the frontend also avoids multiplying fetches, mappings and intermediate states just to display one coherent business view.

The detail that matters in staging

This kind of solution looks small, but its runtime details matter.

The good example is the secure flag. In production, wanting a secure cookie is correct. But in deployments where TLS is terminated upstream, the reasoning still has to match the real transport chain.

You can see the same concern in auth routes and in middleware that reads x-forwarded-proto on the portal side. That is not an isolated infra detail. It is part of the web auth design.

What I want other developers to take away

I do not want to show “one more cookie”. I want to show a decision rule.

A BFF and Next.js routes are worth it when they:

  • remove the token from application-side JavaScript
  • make the web session easier to reason about
  • centralise the conversion point between API auth and web auth
  • give the frontend a simpler boundary to consume
  • harmonise contracts across services
  • return data that is already more useful to the UI

If they absorb no real complexity, they are just another layer.

Here, they absorb two useful responsibilities: securing the web boundary and giving the frontend better-structured data. That is why the pattern deserves its place.

Keep reading

A few related articles to extend the topic and keep the internal linking strong.

3 min

Two Next.js apps on two domains, one shared API

Why we split the customer portal and the operator console into two Next.js apps, and what that separation removed from the codebase.

  • Architecture
  • Multi-tenant
  • Auth
Read the note