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
localStorageor 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:
- the browser posts credentials to a Next.js route
- that route calls the BFF
- the BFF talks to the business API
- the Next.js route writes an
httpOnlycookie - 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
tokenoraccess_token - write the
authTokencookie
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.