How to Fix Random Logouts in Supabase + Next.js Apps: The Session Refresh Guide
Your Supabase + Next.js app works fine locally, but users report getting logged out after a few hours in production. The culprit: your middleware isn't refreshing expired sessions. Here's the exact pattern that fixes it.
The Problem That Breaks Production Auth
You launch your Next.js app backed by Supabase. Everything works locally. Users sign up, they're logged in, everything looks good. Then, after an hour or two in production, support tickets start rolling in: "I got randomly logged out" or "My session expired even though I was using the app."
This is one of the most common production bugs in Supabase + Next.js applications, and it's almost always the same root cause: your middleware isn't properly handling session refresh.
Here's what's happening under the hood. Supabase sessions use short-lived access tokens (by default, 1 hour). When that token expires, your app has a refresh token stored in cookies that can get a new access token. The problem is, if you're not explicitly refreshing the session in your middleware and passing those updated cookies back to the user's browser, the refresh token never makes it back. The user's browser still has the old, expired token. Next click? Logged out.
This is especially painful because it works fine during local development. You're testing with the service role key or working with test accounts where sessions don't expire. Ship to production with real users? Suddenly everyone's getting kicked out.
Why Middleware is the Critical Layer
Most developers think of Next.js middleware.ts as just a router guard. It's not. Middleware runs on every request at the Edge and sits between the user's browser and your application. It's the only place where you can intercept requests, refresh the session, and send updated cookies back to the browser before the page even loads.
If you skip this step and only handle authentication in your page components or API routes, you're already too late. The user's browser will have stale tokens. Middleware is your last chance to keep the session fresh.
The fix is straightforward, but you have to get it right: every request through middleware should check if the Supabase session is still valid, refresh it if needed, and update the cookies in the response.
The One-Fix Solution: Proper Session Refresh in Middleware
Here's the middleware pattern that actually works:
```typescript
import { createServerClient } from '@supabase/ssr'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export async function middleware(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) {
cookiesToSet.forEach(({ name, value, options }) => {
response.cookies.set(name, value, options)
})
},
},
}
)
const { data: { user } } = await supabase.auth.getUser()
return response
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|.*\\.svg).*)',
],
}
```
The key here is the setAll callback in the cookie configuration. When Supabase refreshes the session, it writes the new cookies to the response object. You're not just reading cookies from the request; you're actively writing refreshed cookies back.
This runs on every request. If the session is still valid, nothing changes. If the refresh token is still good and the access token expired, Supabase automatically grabs a fresh one and updates the cookies. The user never sees a logout because the new tokens are already in place before the page loads.
Common Mistakes That Break This Pattern
One mistake developers make: they create the Supabase client in middleware but never actually call a method that would trigger a refresh. Just creating the client doesn't refresh anything. You need to call getUser() or some other auth method that validates the session. That's what triggers Supabase to check if the token is expired and refresh if needed.
Another mistake: not catching errors in middleware. If Supabase throws an error during refresh, your middleware crashes and the user gets a 500. Wrap the auth check in a try-catch and fail gracefully.
```typescript
try {
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
// User is not authenticated, redirect to login
return NextResponse.redirect(new URL('/login', request.url))
}
} catch (error) {
// Refresh failed, redirect to login
return NextResponse.redirect(new URL('/login', request.url))
}
```
A third mistake: protecting your middleware with a matcher that's too aggressive. If your matcher is too broad, you're running this auth check on every single request, including static assets and API routes that don't need it. Use a matcher that targets actual page routes.
Building Production Apps With AI Assistance
This is the kind of problem that trips up developers building with AI assistants like Claude. You ask Claude to "build a Supabase + Next.js auth system" and it generates code that works locally. But production is different. Production has real users, real session timeouts, and real edge cases.
When you're using AI to scaffold your application, you need to explicitly ask for production patterns. Ask for middleware session refresh. Ask for error handling. Ask for retry logic. The default generated code often skips these because they're not technically required for a demo to work.
This is where structured scaffolding matters. Tools like ZipBuild specifically generate production-ready templates that include these patterns by default, so you're not starting with demo code and trying to harden it later.
Testing Your Session Refresh
Before shipping to production, test this locally. Open DevTools and look at your cookies. Make a request through middleware. You should see the auth cookies get refreshed. Wait longer than your token expiration time (modify the JWT expiration if needed for testing) and make another request. Your app should stay logged in.
Also test the failure case: delete the refresh token from cookies and make a request. Your middleware should catch this and redirect to login.
The Takeaway
Random logouts in production Supabase + Next.js apps almost always point to the same issue: sessions aren't being refreshed in middleware. Fix your middleware cookie handling and your sessions will stay alive.
This is the kind of production-hardening that separates prototype code from shipping code. When you're building with AI assistants, always ask for the production version.
Try the free discovery chat at zipbuild.dev to see how production-ready scaffolds eliminate these debugging sessions.
Written by ZipBuild Team
Ready to build with structure?
Try the free discovery chat and see how ZipBuild architects your idea.
Start Building