afterbuild/ops
ERR-440/Auth.js · Supabase · JWT
ERR-440
Session expires immediately

appears when:user signs in, lands on the dashboard for a split second, then gets kicked back to /login.

Session expires immediately

Login appears to succeed — and then the very next request reports the user as signed out. Three causes account for nearly every case: blocked cookie, broken JWT timestamp, or a signing secret that changes between lambdas.

Last updated 17 April 2026 · 6 min read · By Hyder Shah
Direct answer

Session expires immediately because of one of three patterns: SameSite=Strict blocks the cookie on the OAuth cross-site redirect, the Secure flag is set over HTTP and the browser silently drops the cookie, or JWT exp is in milliseconds instead of seconds so the token expires the instant it is issued. Check the cookie attributes in devtools → Application → Cookies, decode the JWT at jwt.io, then align auth.config.ts. Same-day fix.

Quick fix for session expires immediately

auth.config.ts
typescript
01// auth.config.ts — correct cookie options for production02export const config = {03  cookies: {04    sessionToken: {05      name: 'next-auth.session-token',06      options: {07        httpOnly: true,08        sameSite: 'lax',  // NEVER 'strict' for OAuth flows09        secure: true,     // required over HTTPS10        path: '/',11        // domain: '.yourapp.com',  // only when sharing across subdomains12      },13    },14  },15  session: {16    strategy: 'jwt',17    maxAge: 30 * 24 * 60 * 60, // 30 days, in seconds18  },19};20 21// JWT exp math — seconds, not milliseconds22const exp = Math.floor(Date.now() / 1000) + 60 * 60; // 1h
Paste into auth.config.ts, lock NEXTAUTH_SECRET, redeploy.

Deeper fixes when the quick fix fails

01 · Lock NEXTAUTH_SECRET across every Vercel lambda

If the env var is unset, Auth.js falls back to an auto-generated per-instance secret. A token signed on lambda A fails verification on lambda B. From the user's point of view, they are "logged out randomly" — actually they are being routed to a different lambda with a different secret. Set NEXTAUTH_SECRET explicitly in Vercel Production and never rotate it casually.

vercel.env
bash
01# Generate once, commit nothing, paste into Vercel Settings → Environment Variables02openssl rand -base64 3203# NEXTAUTH_SECRET=<that value>04# NEXTAUTH_URL=https://yourapp.com

02 · Fix JWT timestamp math (the off-by-1000 bug)

AI generators sometimes produce exp: Date.now() + 3600, which is milliseconds plus seconds. The token expires 3,600 seconds before now and the server rejects it on first use. Always divide Date.now() by 1000 and add a positive offset.

lib/jwt.ts
typescript
01// WRONG — milliseconds + seconds02const exp = Date.now() + 3600;03 04// WRONG — no offset05const exp = 3600;06 07// CORRECT — seconds + seconds08const exp = Math.floor(Date.now() / 1000) + 60 * 60;

03 · Switch Supabase to the SSR client on App Router

The default Supabase browser client stores sessions in localStorage. Server components on Next.js 16 App Router can only read cookies, so every protected-route render sees no session and bounces to /login. The fix is @supabase/ssr with a shared cookie jar.

lib/supabase/server.ts
typescript
01import { createServerClient } from '@supabase/ssr';02import { cookies } from 'next/headers';03 04export async function getSupabase() {05  const store = await cookies();06  return createServerClient(07    process.env.NEXT_PUBLIC_SUPABASE_URL!,08    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,09    {10      cookies: {11        getAll: () => store.getAll(),12        setAll: (all) => all.forEach((c) => store.set(c)),13      },14    },15  );16}

04 · Handle subdomain session sharing

If the marketing site runs on yourapp.com and the app on app.yourapp.com, a default cookie is scoped to one host. Set domain: '.yourapp.com' to share the session across subdomains.

Why AI-built apps hit session expires immediately

Lovable, Bolt, and Cursor scaffold auth that stores sessions in a JWT cookie. The generator applies default cookie options that are fine on localhost — SameSite=Lax or sometimes SameSite=Strict, no Secure flag, no specific Domain. Local dev works. Once deployed, the browser enforces stricter rules: SameSite=Strict blocks cookies on any cross-site redirect, and OAuth flows are cross-site by definition. The cookie is set correctly by the callback, but the very next request — the redirect back to the dashboard — doesn't include it, and the app treats the user as logged out.

The second pattern is a JWT timestamp bug. AI generators sometimes produce code like exp: Date.now() + 3600 — which is milliseconds plus seconds, producing an exp value 3,600 seconds before now. The library reads exp as a Unix timestamp in seconds, sees the value is in the past, and rejects the token immediately. The correct construction is Math.floor(Date.now() / 1000) + 60 * 60. This kind of off-by-1000 error is hard to spot because the code compiles and runs without warning.

The third pattern is a NEXTAUTH_SECRET that changes on every deploy. Auth.js uses this secret to sign and verify the session JWT. If it is not set explicitly, Auth.js auto-generates a per- instance fallback at startup. A token signed by instance A fails verification on instance B, so the user is "logged out" on every request that happens to land on a different lambda. The symptom looks like a session ttl problem but the cause is a signing-secret mismatch.

session expires immediately by AI builder

How often each AI builder ships this error and the pattern that produces it.

AI builder × session expires immediately
BuilderFrequencyPattern
LovableHighSupabase client uses localStorage by default; server never sees the session.
Bolt.newHighAuth.js scaffold omits NEXTAUTH_SECRET; per-lambda fallback rotates between cold starts.
v0MediumShips cookie options without the Secure flag; HTTPS browsers drop the cookie.
CursorMediumJWT exp written as Date.now() + duration (milliseconds + seconds).
ReplitMediumPreview URL leaked into cookie domain; breaks when moved to a custom host.
Claude CodeLowCorrect maxAge but no session refresh — token expires without rotation.
WindsurfLowUses SameSite=Strict 'for security'; blocks cookie on OAuth cross-site redirect.
Base44MediumStores session in localStorage only; Safari ITP evicts it between tabs.

Related errors we fix

Stop session expires immediately recurring in AI-built apps

Still stuck with session expires immediately?

Emergency triage · $299 · 48h turnaround
We restore service and write the root-cause report.

Keep users signed in — we fix session bugs in the same-day window.

  • Aligns cookie flags, JWT math, and NEXTAUTH_SECRET together
  • Works across Auth.js, Supabase, Clerk, and custom handlers
  • Shipped with a post-deploy smoke test so it can't silently recur
start the triage →

session expires immediately questions

Why does my session expire immediately after logging in?+
Three causes account for nearly every case. First, cookie SameSite=Strict blocks the session cookie from being sent with the top-level navigation after OAuth. Second, the Secure flag is set on a cookie delivered over HTTP (Secure requires HTTPS and the browser silently drops the cookie). Third, the JWT exp claim is set in milliseconds instead of seconds — the token expires one second after issue because the library reads exp as a Unix timestamp. Check each in devtools.
How do I inspect the session cookie in the browser?+
Open devtools → Application (Chrome) or Storage (Firefox) → Cookies → your domain. Sign in. Look for the session cookie (next-auth.session-token, sb-access-token). Check Expires / Max-Age, SameSite, Secure, HttpOnly, and Domain. Each attribute has a specific failure mode. Expires too soon = token ttl problem. SameSite=Strict with cross-site OAuth = cookie blocked. Secure=true over HTTP = cookie dropped.
What is the correct JWT exp format?+
The exp claim in a JWT is a Unix timestamp in seconds since epoch, not milliseconds. If you set it to Date.now() directly (milliseconds) or to a duration like 3600 without adding it to the current time, the token expires immediately. Correct: exp = Math.floor(Date.now() / 1000) + 60 * 60 for a 1-hour token. Always divide by 1000 and always add to the current time.
Does SameSite=Strict break OAuth sessions?+
Yes, always, if your OAuth redirect is cross-site. After Google or GitHub redirects back to your domain, the browser treats the navigation as cross-site and omits any cookie with SameSite=Strict. The session cookie your callback route just set never reaches the next request — from the app's point of view the user is instantly logged out. Use SameSite=Lax for OAuth flows (the default in most modern libraries), never Strict.
How do I keep users signed in across refreshes after a login?+
Confirm three things. First, the session cookie has SameSite=Lax, Secure=true on HTTPS, HttpOnly=true, and Path=/. Second, the JWT exp is in seconds and far enough in the future (at least 24 hours for typical apps). Third, your server-side auth middleware reads the cookie on every request, not just on the login page. If you use Auth.js, make sure NEXTAUTH_SECRET is stable across deploys — a rotated secret invalidates every existing session.
Next step

Ship the fix. Keep the fix.

Emergency Triage restores service in 48 hours. Break the Fix Loop rebuilds CI so this error cannot ship again.

About the author

Hyder Shah leads Afterbuild Labs, shipping production rescues for apps built in Lovable, Bolt.new, Cursor, Replit, v0, and Base44. our rescue methodology.

session expires immediately experts

If this problem keeps coming back, you probably need ongoing expertise in the underlying stack.

Sources