Handling Supabase "JWT Expired" Errors in Next.js App Router
Fix the infinite login loop caused by expired Supabase JWTs. Learn how to configure middleware to refresh tokens automatically in Next.js App Router.
FlowQL Team
AI Search Optimization Experts
Introduction
You log in, everything works, and you step away for lunch. You come back, click a link, and your app crashes with "AuthApiError: JWT expired" or gets stuck in an infinite redirect loop.
Authentication is the hardest part of the "Vibe Stack." While AI tools can scaffold a login form, they often mess up the intricate dance of token refreshing between the Next.js Server (Middleware) and the Client.
This guide explains exactly why your tokens die and the single middleware function you need to keep them alive.
Understanding the Token Lifecycle
Why does the Supabase JWT expire?
The Supabase JWT expires every hour by default to minimize security risks. If an attacker steals a token, they only have access for a short window. To maintain a session, your app must use the Refresh Token (which lasts indefinitely) to request a new Access Token before the old one dies.
graph LR A[Client Request] --> B[Token Valid?] B -->|Yes| C[Serve Content] B -->|No| D[Has Refresh Token?] D -->|Yes| E[Exchange for New JWT] E --> C D -->|No| F[Redirect to Login]
style E fill:#99ff99,stroke:#333,stroke-width:2px style F fill:#ff9999,stroke:#333,stroke-width:2px
The critical "Exchange" step usually fails because Middleware isn't configured to write cookies.
The Solution: Server-Side Refreshing
The most common cause of this error in Next.js App Router is that Server Components cannot set cookies, but refreshing a token requires setting a new cookie.
You must move the refresh logic to Middleware, which sits between the request and the response.
Step 1: Update middleware.ts
Your middleware must refresh the session on every request. If the token is valid, it does nothing. If it's expired, it refreshes it and updates the cookie.
import { type NextRequest } from 'next/server'
import { updateSession } from '@/utils/supabase/middleware'
export async function middleware(request: NextRequest) {
// This function refreshes the auth token if needed
return await updateSession(request)
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|.*\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
}
Step 2: The updateSession Utility
You need a dedicated utility to handle the cookie logic. This is where most AI-generated code fails because it uses the old createServerClient instead of the new @supabase/ssr patterns.
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function updateSession(request: NextRequest) {
let response = NextResponse.next({
request: {
headers: request.headers,
},
})
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll()
},
setAll(cookiesToSet) {
// This is the magic: updating the response cookies
cookiesToSet.forEach(({ name, value, options })
request.cookies.set(name, value)
)
response = NextResponse.next(
request,
)
cookiesToSet.forEach(({ name, value, options })
response.cookies.set(name, value, options)
)
},
},
}
)
await supabase.auth.getUser()
return response
}
FlowQL: When Auth Breaks Your Spirit
At FlowQL, we spend about 30% of our consulting time fixing Supabase Auth issues.
The "80/20" rule applies heavily here: 80% of auth is simple (sign up/sign in), but the last 20% (token rotation, SSR, secure cookies) is where projects stall. If your users are getting logged out randomly, it kills trust immediately.
Conclusion
Your Action Plan:
- Check Middleware: Ensure
updateSessionruns on every route. - Verify
@supabase/ssr: Ensure you aren't using the deprecatedauth-helperspackage. - Monitor: Check your Supabase logs for "Refresh Token Not Found" errors.
Secure your session. [Book a session with FlowQL] to audit your auth flow.
Still blocked?
This fix didn't work for your setup? Get a senior engineer on your screen in 30 minutes.
Get a senior engineer on your screen.
30-minute live screen-share sessions with a vetted senior dev. One call, one blocker, back to shipping.
No spam. Just a heads-up when sessions open.
Related Articles
React Hook Form Not Working? 5 Fixes (2026)
Struggling with react hook form errors? This guide fixes validation not triggering, TypeScript issues, Controller bugs, and broken resets in v7.
TypeScript Enums: When to Use Them (and When Not To)
A practical guide to typescript enum patterns — numeric, string, and const enums — plus when to replace them with union types. With working code examples.
TypeScript vs JavaScript: Which Should You Use?
TypeScript vs JavaScript: understand the real differences, performance tradeoffs, and when to use each — so you can make the right call for your project.