The Next.js App Router introduced a constraint that breaks most auth libraries: Server Components cannot write cookies. Only Route Handlers and Server Actions can modify the cookie jar.
This means if your Server Component calls an API, gets a 401, refreshes the token, and tries to save the new token — it fails silently. The refresh succeeds but the cookie never updates.
The solution: proactive refresh in middleware
Instead of reacting to 401s, check the token before the page renders. Next.js middleware runs before every request, can read AND write cookies, and can make fetch calls.
// middleware.ts
import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"
export async function middleware(request: NextRequest) {
const access = request.cookies.get("access_token")?.value
const refresh = request.cookies.get("refresh_token")?.value
if (!access && refresh) {
const res = await fetch(`${process.env.API_BASE_URL}/auth/refresh`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ refresh }),
})
if (res.ok) {
const { access: newToken } = await res.json()
const response = NextResponse.next()
response.cookies.set("access_token", newToken, {
httpOnly: true, secure: true, sameSite: "lax", path: "/",
})
return response
}
// Refresh failed — session is dead
const response = NextResponse.redirect(new URL("/login", request.url))
response.cookies.delete("access_token")
response.cookies.delete("refresh_token")
return response
}
return NextResponse.next()
}The tower/passenger pattern
With middleware handling refresh, your glyde instances become simple:
- tower (server) — reads cookies, adds Authorization header via interceptor
- passenger (client) — calls /api/proxy routes, redirects on 401
Neither instance needs to know about token refresh. Middleware handles it transparently before any code runs.
Why this is better than built-in refresh
- No cookie-writing constraint — middleware can write cookies
- No double-request penalty — token is fresh before SSR starts
- No mutex/queue complexity — one refresh per request cycle
- Framework-aligned — uses Next.js primitives, not library hacks
Key takeaway
Don't fight the framework. Next.js middleware exists precisely for this use case. Let glyde handle the HTTP layer (interceptors, errors, types) and let middleware handle the auth lifecycle (refresh, redirect, cookie management).