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 expiredThe 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 FoundThe 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 UnauthorizedAn 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
Add an auth state listener at the app root
The Supabase client needs a persistent listener to detect token expiry and trigger automatic refresh
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.
import { BrowserRouter, Routes, Route } from "react-router-dom";import Index from "./pages/Index";function App() { return ( <BrowserRouter> <Routes> <Route path="/" element={<Index />} /> </Routes> </BrowserRouter> );}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.
Ensure only one Supabase client instance exists
Multiple client instances create competing refresh cycles that can invalidate each other's tokens
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.
// src/pages/Dashboard.tsx - BAD: creating a second clientimport { createClient } from "@supabase/supabase-js";const supabase = createClient( import.meta.env.VITE_SUPABASE_URL, import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY);// src/pages/Dashboard.tsx - GOOD: import the shared clientimport { supabase } from "@/integrations/supabase/client";// Use this single instance for all Supabase operations// The shared client in client.ts handles token refresh globallyExpected result: All components use the same Supabase client instance, preventing conflicting token refresh cycles.
Handle session recovery after long inactivity
When both the access and refresh tokens expire during extended inactivity, the user must re-authenticate gracefully
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.
async function fetchUserData() { const { data, error } = await supabase .from("profiles") .select("*") .single(); // No error handling — app breaks silently on expired session setProfile(data);}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.
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
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
1import { useEffect, useState } from "react";2import { useNavigate } from "react-router-dom";3import { Session, AuthChangeEvent } from "@supabase/supabase-js";4import { supabase } from "@/integrations/supabase/client";56export function useAuth(requireAuth = true) {7 const navigate = useNavigate();8 const [session, setSession] = useState<Session | null>(null);9 const [isLoading, setIsLoading] = useState(true);1011 useEffect(() => {12 // Fetch current session on mount13 supabase.auth.getSession().then(({ data: { session } }) => {14 setSession(session);15 setIsLoading(false);1617 if (requireAuth && !session) {18 navigate("/login", { replace: true });19 }20 });2122 // Subscribe to all auth state changes23 const { data: { subscription } } = supabase.auth.onAuthStateChange(24 (event: AuthChangeEvent, session: Session | null) => {25 setSession(session);2627 if (event === "SIGNED_OUT" && requireAuth) {28 navigate("/login", { replace: true });29 }3031 if (event === "TOKEN_REFRESHED") {32 // Token was refreshed successfully — no action needed33 console.log("Session token refreshed");34 }35 }36 );3738 return () => subscription.unsubscribe();39 }, [navigate, requireAuth]);4041 const signOut = async () => {42 await supabase.auth.signOut();43 navigate("/login", { replace: true });44 };4546 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.
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]
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your issue.
Book a free consultation