Discover why session expiration occurs in Lovable and learn best practices for managing token expiry, session state, and secure token handling.

Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
Session expiration happens because authentication artifacts (tokens, cookies, server sessions) are time-limited and your app often doesn't have the durable storage, correct origin/secret configuration, or server-side alignment in Lovable Preview/Cloud to keep them valid — so tokens simply become invalid or inaccessible and the client sees a logged-out state.
// Prompt 1: Create an explanation doc describing why sessions expire in Lovable
Create file docs/session-expiration.md with the following content:
# Why sessions expire in Lovable
Explain that tokens and cookies are time-limited, that Preview can change cookie behavior, that Secrets must be configured in Lovable Cloud, and list the common causes:
- Token TTL and provider-side revocation
- In-memory storage vs durable storage
- SameSite / third-party cookie issues in Preview
- Missing/wrong Secrets or environment variables
- Server-side session mismatches and CORS/CSP blocking silent refresh
- Clock skew
Do not add refresh or token-handling code here — this is documentation only.
// Prompt 2: Add a non-invasive UI notice component that links to the doc
Create file src/components/SessionExpirationNotice.tsx with a small React component that renders a short user-facing explanation and a link to /docs/session-expiration.md.
Then update src/App.tsx to import and render <SessionExpirationNotice /> in the top-level layout (for example, inside the main header or app shell), so users in Preview see the explanation when they are logged out.
Component text should explain why sessions can expire in Preview/Cloud and point to the new docs page. Do not implement any token-refresh logic here.
This prompt helps an AI assistant understand your setup and guide you through the fix step by step, without assuming technical knowledge.
Implement a small token manager + fetch wrapper + React session context so Lovable can automatically refresh before expiry, retry failed calls, and surface session state. Use Lovable Secrets UI to store API base URL. Treat this as a client-driven refresh flow (tokens in memory/localStorage); if you need HttpOnly cookies or server rotation, ask to add a server endpoint later via GitHub sync.
Paste this into Lovable chat. Ask Lovable to create or update the listed files with the exact code; these files use localStorage for refresh tokens and schedule silent refreshes. Adjust names if your app uses a different structure.
// token storage and refresh scheduling
const STORAGE_KEY = 'app_auth';
type Tokens = {
accessToken: string | null;
refreshToken: string | null;
expiresAt: number | null; // epoch ms
};
let refreshTimer: number | null = null;
function load(): Tokens {
try {
const raw = localStorage.getItem(STORAGE_KEY);
return raw ? JSON.parse(raw) : { accessToken: null, refreshToken: null, expiresAt: null };
} catch {
return { accessToken: null, refreshToken: null, expiresAt: null };
}
}
function save(t: Tokens) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(t));
}
export function setTokens({ accessToken, refreshToken, expiresInMs }: { accessToken: string; refreshToken: string; expiresInMs: number; }) {
const expiresAt = Date.now() + expiresInMs;
save({ accessToken, refreshToken, expiresAt });
scheduleRefresh();
}
export function clearTokens() {
localStorage.removeItem(STORAGE_KEY);
if (refreshTimer) { clearTimeout(refreshTimer); refreshTimer = null; }
}
export function getAccessToken() {
return load().accessToken;
}
export async function refreshTokens(): Promise<boolean> {
const s = load();
if (!s.refreshToken) return false;
try {
const res = await fetch((process.env.NEXT_PUBLIC_API_BASE || '') + '/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken: s.refreshToken }),
});
if (!res.ok) return false;
const { accessToken, refreshToken, expiresInMs } = await res.json();
setTokens({ accessToken, refreshToken, expiresInMs });
return true;
} catch {
return false;
}
}
function scheduleRefresh() {
const s = load();
if (!s.expiresAt || !s.refreshToken) return;
const msUntilExpire = s.expiresAt - Date.now();
const refreshBefore = Math.max(5000, Math.floor(msUntilExpire * 0.2)); // refresh at 20% before expiry or 5s
if (refreshTimer) clearTimeout(refreshTimer);
refreshTimer = window.setTimeout(() => { refreshTokens(); }, Math.max(1000, msUntilExpire - refreshBefore));
}
// call on app load
if (typeof window !== 'undefined') {
scheduleRefresh();
}
// api fetch wrapper that retries once after refresh
import { getAccessToken, refreshTokens } from './tokenManager';
export async function apiFetch(input: RequestInfo, init: RequestInit = {}) {
const token = getAccessToken();
const headers = new Headers(init.headers || {});
if (token) headers.set('Authorization', `Bearer ${token}`);
headers.set('Content-Type', headers.get('Content-Type') || 'application/json');
let res = await fetch(input, { ...init, headers });
if (res.status === 401) {
const ok = await refreshTokens();
if (ok) {
const t = getAccessToken();
if (t) headers.set('Authorization', `Bearer ${t}`);
res = await fetch(input, { ...init, headers });
}
}
return res;
}
// React session provider and hook
import React, { createContext, useContext, useEffect, useState } from 'react';
import { setTokens, clearTokens, refreshTokens, getAccessToken } from '../lib/tokenManager';
import { apiFetch } from '../lib/apiClient';
const SessionContext = createContext<any>(null);
export function SessionProvider({ children }: { children: React.ReactNode; }) {
const [user, setUser] = useState<any>(null);
const [loading, setLoading] = useState(true);
async function loadUser() {
const token = getAccessToken();
if (!token) { setUser(null); setLoading(false); return; }
try {
const res = await apiFetch((process.env.NEXT_PUBLIC_API_BASE || '') + '/me');
if (res.ok) {
setUser(await res.json());
} else {
setUser(null);
}
} catch {
setUser(null);
} finally {
setLoading(false);
}
}
useEffect(() => { loadUser(); }, []);
const signIn = async ({ accessToken, refreshToken, expiresInMs }: any) => {
setTokens({ accessToken, refreshToken, expiresInMs });
await loadUser();
};
const signOut = () => {
clearTokens();
setUser(null);
};
const value = { user, loading, signIn, signOut, refreshTokens };
return <SessionContext.Provider value={value}>{children}</SessionContext.Provider>;
}
export const useSession = () => useContext(SessionContext);
Open Lovable Cloud Secrets UI and set a secret:
Use Preview to exercise login, then simulate short expiry by setting expiresInMs to a small value on signIn. The apiFetch wrapper will refresh automatically before expiry and retry on 401. If you need server-side rotation with HttpOnly cookies, tell me and I’ll produce a GitHub sync plan because that requires backend code/dependency installs outside Lovable.
The short answer: keep all sensitive tokens out of client code, store them in Lovable Cloud Secrets, move token logic into server-side code (server endpoints or serverless functions), expose only minimal session checks to the browser (via HttpOnly cookies or small-safe session flags), and use Lovable’s Preview/Publish + GitHub sync for any changes that require deeper platform access.
// Prompt A: Create a server-side session endpoint that uses Lovable Secrets
// Create file src/server/session.ts with this code and wire it into the project's server routing.
// If your framework uses pages/api or /api routes, create pages/api/session.ts instead.
// This endpoint must read process.env.SESSION_SECRET (created in Lovable Secrets) and never expose raw tokens.
export async function handler(req, res) {
// // Server-side: use process.env.SESSION_SECRET (set via Lovable Secrets UI)
const secret = process.env.SESSION_SECRET;
// // Use secret to validate or sign session info - keep raw tokens off the client
// // Return only a safe session flag or set an HttpOnly cookie
res.setHeader('Set-Cookie', `session=1; HttpOnly; Path=/; SameSite=Lax`);
res.json({ ok: true, authenticated: true });
}
// Prompt B: Move client code to call the server endpoint and keep no secret reads in client files
// Create src/lib/authClient.ts and update components to call /api/session for auth checks.
// Do not persist raw tokens in localStorage/sessionStorage.
export async function checkSession() {
const resp = await fetch('/api/session', { credentials: 'include' });
return resp.json();
}
// // Update UI components to call checkSession() on mount and rely on the returned authenticated flag.
// Prompt C: Add Secrets in Lovable Cloud
// Instruct the user to open Lovable Cloud > Secrets (UI) and add:
// - SESSION_SECRET (set to your signing key or API token value)
// - OPTIONAL_API_KEY (if you must talk to an upstream service from server-side)
// Note: do not add secrets as client-side env variables. Use only from server code (process.env.*).
// Prompt D: Preview and publish
// After applying code changes and adding Secrets, run Preview in Lovable to validate endpoints read secrets.
// If everything passes, click Publish to deploy the changes.
From startups to enterprises and everything in between, see for yourself our incredible impact.
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.