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

Server
1

Submit form

signInEmail() server action receives FormData

2

Validate input

Zod loginSchema checks email format + min password length

3

Authenticate

supabase.auth.signInWithPassword() verifies credentials

4

Create session

createSession(userId) inserts row into user_sessions with 30-min expiry

5

Redirect

Server redirects to /dashboard

Google OAuth

Server
1

Click OAuth button

signInGoogle() server action called via useTransition

2

OAuth redirect

supabase.auth.signInWithOAuth() returns Google consent URL

3

Google callback

Google redirects to /auth/callback?code=XXX

4

Exchange code

Route handler exchanges the code for a session, then creates DB record

5

Redirect

Server redirects to /dashboard

02

Cookie & session storage

Auth state is split across two stores — a JWT cookie managed by Supabase, and a DB record that enables server-side expiry control.

Supabase Auth Cookie

Server
Namesb-<ref>-auth-token
TypeJWT (signed by Supabase)
Expiry1800 s (30 min) per dashboard config
Set bySupabase SSR automatically on login
Refreshsupabase.auth.refreshSession()

DB Record

Database
Table: user_sessions
id             uuid  (PK)
user_id        uuid  → auth.users
last_active_at timestamptz
created_at     timestamptz
expires_at     timestamptz  ← now + 30 min

extendSession() updates last_active_at + expires_at. invalidateSession() deletes the row.

03

3-layer session validation

Every request to a protected route passes through three independent guards before content is served.

1

Middleware

Server
  • Reads Supabase auth cookie — missing → redirect /login
  • Queries user_sessions.expires_at — expired → delete row + signOut + redirect /login
2

Dashboard Layout

Server
  • Re-validates user via supabase.auth.getUser()
  • Re-fetches session row — expired → invalidateSession + signOut + redirect /login
3

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.

1
Client

Click "Stay signed in"

User responds to IdleWarningModal before 180 s countdown reaches 0

2
Server

extendSession() server action

Delegates to extendSessionLib() in lib/session.ts

3
Database

DB row updated

last_active_at = now, expires_at = now + 30 min

4
Server

JWT refreshed

supabase.auth.refreshSession() issues new JWT, cookie updated automatically

5
Client

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

Client
1

10 min of inactivity

No tracked events received

2

Warning modal appears

IdleWarningModal mounts with 180 s countdown

3

Countdown expires

User did not click Stay — countdown reached 0

4

forceLogout(userId)

Deletes DB session row and clears Supabase JWT cookie

5

Redirect

Client navigates to /login?reason=expired

Hard limit path

Client
1

30 min elapsed

Calculated from session.created_at passed to SessionProvider

2

Hard limit timer fires

useIdleSession setTimeout triggers at remaining ms from login

3

forceLogout(userId)

Same action as idle path — deletes session + signOut

4

Redirect

Client navigates to /login?reason=expired