auth.flow — How it works
Session Management, end-to-end.
A walkthrough of every layer — from login to cookie storage, session validation, idle detection, and automatic logout.
01
Login flow
Two paths — email/password and Google OAuth — both terminate at the same session creation step.
Email / Password
ServerSubmit form
signInEmail() server action receives FormData
Validate input
Zod loginSchema checks email format + min password length
Authenticate
supabase.auth.signInWithPassword() verifies credentials
Create session
createSession(userId) inserts row into user_sessions with 30-min expiry
Redirect
Server redirects to /dashboard
Google OAuth
ServerClick OAuth button
signInGoogle() server action called via useTransition
OAuth redirect
supabase.auth.signInWithOAuth() returns Google consent URL
Google callback
Google redirects to /auth/callback?code=XXX
Exchange code
Route handler exchanges the code for a session, then creates DB record
Redirect
Server redirects to /dashboard
03
3-layer session validation
Every request to a protected route passes through three independent guards before content is served.
Middleware
Server- →Reads Supabase auth cookie — missing → redirect /login
- →Queries user_sessions.expires_at — expired → delete row + signOut + redirect /login
Dashboard Layout
Server- →Re-validates user via supabase.auth.getUser()
- →Re-fetches session row — expired → invalidateSession + signOut + redirect /login
useIdleSession hook
Client- →Hard limit timer fires at 30 min from sessionCreatedAt
- →Calls onExpire() → forceLogout(userId) → redirect /login
04
Idle detection
Client-side event listeners track user activity. Prolonged inactivity triggers a warning modal before forcing logout.
0 min
login
activity
resets timer
10 min idle
IDLE_TIMEOUT
warning modal
180 s countdown
auto logout
if no response
If the user clicks Stay signed in during the warning modal, the session is extended and the timer resets.
Tracked events
mousemove mousedown keydown scroll touchstart
Timing constants
IDLE_TIMEOUT = 10 min WARNING_DURATION = 180 s HARD_LIMIT = 30 min (from login)
05
Extend session
When the user clicks Stay signed in, a full chain of updates happens across client, server, and database.
Click "Stay signed in"
User responds to IdleWarningModal before 180 s countdown reaches 0
extendSession() server action
Delegates to extendSessionLib() in lib/session.ts
DB row updated
last_active_at = now, expires_at = now + 30 min
JWT refreshed
supabase.auth.refreshSession() issues new JWT, cookie updated automatically
Modal dismissed, timers reset
clearWarning() hides modal and calls resetIdle() to restart timers
06
Auto logout
Two independent paths both call the same forceLogout() action, ensuring cleanup regardless of how expiry is triggered.
Idle timeout path
Client10 min of inactivity
No tracked events received
Warning modal appears
IdleWarningModal mounts with 180 s countdown
Countdown expires
User did not click Stay — countdown reached 0
forceLogout(userId)
Deletes DB session row and clears Supabase JWT cookie
Redirect
Client navigates to /login?reason=expired
Hard limit path
Client30 min elapsed
Calculated from session.created_at passed to SessionProvider
Hard limit timer fires
useIdleSession setTimeout triggers at remaining ms from login
forceLogout(userId)
Same action as idle path — deletes session + signOut
Redirect
Client navigates to /login?reason=expired