Skip to main content
RapidDev - Software Development Agency
lovable-issues

Fixing Token Expiration and Session Management in Lovable

Token expiration in Lovable apps happens when Supabase session refresh is not configured properly. Fix it by calling supabase.auth.onAuthStateChange() at the app root to automatically refresh JWT tokens, setting appropriate session expiry in your Supabase dashboard, and handling the TOKEN_REFRESHED event to keep users logged in seamlessly.

Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate9 min read~15 minLovable projects using Supabase AuthMarch 2026RapidDev Engineering Team
TL;DR

Token expiration in Lovable apps happens when Supabase session refresh is not configured properly. Fix it by calling supabase.auth.onAuthStateChange() at the app root to automatically refresh JWT tokens, setting appropriate session expiry in your Supabase dashboard, and handling the TOKEN_REFRESHED event to keep users logged in seamlessly.

Why sessions expire unexpectedly in Lovable apps

Lovable projects use Supabase for authentication, which issues JWT (JSON Web Token) access tokens with a default expiry of one hour. When that token expires and your app does not refresh it, every authenticated request starts failing — the user appears logged out even though they never clicked 'sign out'. The Supabase client library includes built-in token refresh logic, but it only works if you set up an auth state listener at the top level of your React app. Many Lovable-generated projects either skip this listener or place it inside a component that unmounts during navigation, breaking the refresh cycle. Another common cause is tab backgrounding. When a user leaves your app tab inactive for an extended period, the browser may throttle JavaScript timers. The Supabase client tries to refresh the token before it expires, but if the timer was throttled, the refresh fires too late. The token is already expired, and the refresh token itself may have also expired if the gap is long enough.

  • Missing onAuthStateChange listener at the app root, so token refresh never triggers
  • Auth state listener placed inside a component that unmounts during route changes
  • Browser tab backgrounded for longer than the JWT expiry window, causing both tokens to expire
  • Supabase JWT expiry set too short (under 3600 seconds) for a typical user session
  • Multiple Supabase client instances competing for the same session storage

Error messages you might see

JWT expired

The access token's lifetime has passed without a refresh. Your app needs an active onAuthStateChange listener to renew the token automatically before it expires.

Invalid Refresh Token: Refresh Token Not Found

The refresh token stored in the browser has been revoked or expired. This usually happens after very long inactivity periods. The user must sign in again.

AuthSessionMissingError: Auth session missing!

Supabase cannot find any session data in localStorage. This occurs when getSession() is called before the auth listener has initialized, or after tokens have been cleared.

401 Unauthorized

An API request was made with an expired or missing access token. The Supabase client should refresh the token automatically, but if the listener is not set up, the stale token is sent instead.

Before you start

  • A Lovable project with Supabase authentication enabled
  • At least one sign-in method configured (email, magic link, or OAuth)
  • Access to your Supabase dashboard to check JWT settings
  • The project open in Lovable's editor or Dev Mode

How to fix it

1

Add an auth state listener at the app root

The Supabase client needs a persistent listener to detect token expiry and trigger automatic refresh

Open your main App component (usually src/App.tsx) in Dev Mode or prompt Lovable to edit it. Add a useEffect that calls supabase.auth.onAuthStateChange() when the component mounts. This listener fires on every auth event including TOKEN_REFRESHED, SIGNED_IN, and SIGNED_OUT. Because App.tsx never unmounts, the listener stays active for the entire session. Store the session in React state so child components can access it.

Before
typescript
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Index from "./pages/Index";
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Index />} />
</Routes>
</BrowserRouter>
);
}
After
typescript
import { useEffect, useState } from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { Session } from "@supabase/supabase-js";
import { supabase } from "@/integrations/supabase/client";
import Index from "./pages/Index";
function App() {
const [session, setSession] = useState<Session | null>(null);
useEffect(() => {
// Get the initial session on mount
supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session);
});
// Listen for all auth changes including token refresh
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(_event, session) => {
setSession(session);
}
);
// Clean up the listener when App unmounts
return () => subscription.unsubscribe();
}, []);
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Index />} />
</Routes>
</BrowserRouter>
);
}

Expected result: The auth state listener runs continuously. When the JWT nears expiry, Supabase automatically refreshes it and your app updates seamlessly.

2

Ensure only one Supabase client instance exists

Multiple client instances create competing refresh cycles that can invalidate each other's tokens

Check that your Supabase client is created once in a shared file (typically src/integrations/supabase/client.ts) and imported everywhere else. If multiple files create their own createClient() calls, each instance manages its own session independently. When one refreshes the token, the others still hold the old refresh token, which becomes invalid. Search your project for 'createClient' — you should see exactly one call.

Before
typescript
// src/pages/Dashboard.tsx - BAD: creating a second client
import { createClient } from "@supabase/supabase-js";
const supabase = createClient(
import.meta.env.VITE_SUPABASE_URL,
import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY
);
After
typescript
// src/pages/Dashboard.tsx - GOOD: import the shared client
import { supabase } from "@/integrations/supabase/client";
// Use this single instance for all Supabase operations
// The shared client in client.ts handles token refresh globally

Expected result: All components use the same Supabase client instance, preventing conflicting token refresh cycles.

3

Handle session recovery after long inactivity

When both the access and refresh tokens expire during extended inactivity, the user must re-authenticate gracefully

Add error handling around your authenticated API calls to detect expired sessions. When a request fails with a 401 or AuthSessionMissingError, redirect the user to the sign-in page instead of showing a broken state. Use a try-catch wrapper or a global error handler to centralize this logic.

Before
typescript
async function fetchUserData() {
const { data, error } = await supabase
.from("profiles")
.select("*")
.single();
// No error handling — app breaks silently on expired session
setProfile(data);
}
After
typescript
async function fetchUserData() {
const { data: { session } } = await supabase.auth.getSession();
if (!session) {
// Session is completely gone — redirect to login
navigate("/login", { replace: true });
return;
}
const { data, error } = await supabase
.from("profiles")
.select("*")
.single();
if (error?.message?.includes("JWT expired")) {
// Token expired mid-request — force a refresh attempt
const { error: refreshError } = await supabase.auth.refreshSession();
if (refreshError) {
navigate("/login", { replace: true });
return;
}
// Retry the original request after refresh
const { data: retryData } = await supabase
.from("profiles")
.select("*")
.single();
setProfile(retryData);
return;
}
setProfile(data);
}

Expected result: Users who return after long inactivity are smoothly redirected to sign in again instead of seeing a blank or broken page.

4

Verify JWT expiry settings in the Supabase dashboard

A very short JWT expiry increases the frequency of refresh cycles and the chance of race conditions

Go to your Supabase project dashboard (not the Lovable Cloud tab — the actual Supabase dashboard at supabase.com). Navigate to Authentication and then Settings. Find the JWT expiry setting. The default is 3600 seconds (one hour), which works well for most apps. If someone set this lower (like 300 seconds), increase it back to 3600. Extremely short expiry times cause more frequent token refreshes, which increases the chance of failures especially on slow connections. If this involves changes across multiple auth-related components, RapidDev's engineers have resolved this exact session management pattern across 600+ Lovable projects.

Expected result: JWT expiry is set to a reasonable duration (3600 seconds or higher), reducing unnecessary refresh cycles.

Complete code example

src/hooks/useAuth.ts
1import { useEffect, useState } from "react";
2import { useNavigate } from "react-router-dom";
3import { Session, AuthChangeEvent } from "@supabase/supabase-js";
4import { supabase } from "@/integrations/supabase/client";
5
6export function useAuth(requireAuth = true) {
7 const navigate = useNavigate();
8 const [session, setSession] = useState<Session | null>(null);
9 const [isLoading, setIsLoading] = useState(true);
10
11 useEffect(() => {
12 // Fetch current session on mount
13 supabase.auth.getSession().then(({ data: { session } }) => {
14 setSession(session);
15 setIsLoading(false);
16
17 if (requireAuth && !session) {
18 navigate("/login", { replace: true });
19 }
20 });
21
22 // Subscribe to all auth state changes
23 const { data: { subscription } } = supabase.auth.onAuthStateChange(
24 (event: AuthChangeEvent, session: Session | null) => {
25 setSession(session);
26
27 if (event === "SIGNED_OUT" && requireAuth) {
28 navigate("/login", { replace: true });
29 }
30
31 if (event === "TOKEN_REFRESHED") {
32 // Token was refreshed successfully — no action needed
33 console.log("Session token refreshed");
34 }
35 }
36 );
37
38 return () => subscription.unsubscribe();
39 }, [navigate, requireAuth]);
40
41 const signOut = async () => {
42 await supabase.auth.signOut();
43 navigate("/login", { replace: true });
44 };
45
46 return { session, isLoading, signOut, user: session?.user ?? null };
47}

Best practices to prevent this

  • Always place the onAuthStateChange listener in your root App component so it persists across all route changes and never unmounts
  • Use a single shared Supabase client instance — search your project for createClient() and ensure there is exactly one call
  • Keep JWT expiry at 3600 seconds (the default) unless you have a specific security requirement for shorter sessions
  • Handle the SIGNED_OUT event by redirecting to the login page to prevent users from seeing broken authenticated pages
  • Wrap authenticated API calls in error handlers that detect 401 responses and attempt a session refresh before giving up
  • Test session persistence by leaving your app tab inactive for over an hour, then returning and verifying you are still logged in
  • Never store access tokens in React state alone — Supabase stores them in localStorage automatically, which survives page refreshes

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I have a Lovable.dev app using Supabase Auth and users keep getting logged out unexpectedly. The console shows 'JWT expired' or 'AuthSessionMissingError' errors. Here is my current auth setup: [paste your App.tsx or auth hook code here] Please help me: 1. Check if my onAuthStateChange listener is set up correctly 2. Verify I am not creating multiple Supabase client instances 3. Add proper error handling for expired sessions 4. Suggest the right JWT expiry duration for my use case: [describe your app]

Lovable Prompt

Users are getting logged out unexpectedly in my app. Check @src/App.tsx and @src/integrations/supabase/client.ts to make sure there is exactly one Supabase client instance and that onAuthStateChange is set up in the App component. Add error handling for expired JWT tokens in any components that call supabase.from() or supabase.auth.getSession(). Redirect users to the login page if their session cannot be recovered.

Frequently asked questions

Why do users keep getting logged out of my Lovable app?

The most common cause is a missing onAuthStateChange listener in your root App component. Without this listener, the Supabase client cannot refresh the JWT token when it expires (default: every hour). Add the listener in a useEffect inside App.tsx.

How long do Supabase sessions last in Lovable?

Supabase JWT access tokens expire after 3600 seconds (one hour) by default. The refresh token lasts much longer. As long as your app has an active onAuthStateChange listener, the access token is refreshed automatically before it expires, keeping the user logged in indefinitely.

What is the difference between the access token and refresh token?

The access token is a short-lived JWT (one hour by default) sent with every API request. The refresh token is long-lived and is used to get a new access token when the old one expires. Supabase handles the refresh automatically if you have the onAuthStateChange listener set up.

How do I keep users logged in across page refreshes?

Supabase stores session data in localStorage automatically. On page load, call supabase.auth.getSession() in a useEffect to restore the session. The onAuthStateChange listener will then maintain it going forward. Do not store the session only in React state, as that is lost on refresh.

Why does my app work in preview but sessions expire in production?

Preview and production may use different domains. If your Supabase Site URL is set to the preview domain, the session cookies may not work on the production domain. Update the Site URL in your Supabase Authentication Settings to match your production URL.

Can I extend the JWT expiry time?

Yes. Go to your Supabase dashboard, navigate to Authentication then Settings, and increase the JWT expiry value. The default of 3600 seconds works for most apps. Setting it much higher reduces refresh frequency but means revoked users retain access longer.

What if I can't fix this myself?

Session management that spans multiple auth providers, role-based access, and cross-domain configurations can get complex quickly. RapidDev's engineers have implemented robust session handling across 600+ Lovable projects and can typically resolve token expiration issues in a single session.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your issue.

Book a free consultation

Need help with your Lovable project?

Our experts have built 600+ apps and can solve your issue fast. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.