Request path
Callstack walkthrough
On mount, the React provider issues a GET /api/session with credentials: 'include' so the fw_sid cookie rides along. The response either hydrates currentUser into context or leaves it null, which the router treats as "show the sign-in screen".
show source
// src/app/providers/AuthProvider.tsx export function AuthProvider({ children }: Props) { const [user, setUser] = useState<User | null>(null); useEffect(() => { fetch('/api/session', { credentials: 'include' }) .then(r => r.ok ? r.json() : null) .then(setUser); }, []); return <AuthCtx.Provider value={{ user }}>{children}</AuthCtx.Provider>; }
The route itself is thin: it just returns whatever req.ctx.session the middleware attached. If the middleware short-circuited with a 401, this handler never runs — so there's no auth logic duplicated here.
show source
// src/server/routes/session.ts router.get('/session', verifyToken, (req, res) => { const { session } = req.ctx; res.json({ id: session.userId, email: session.email, role: session.role, exp: session.expiresAt, }); });
This is the trust boundary. verifyToken reads the signed fw_sid cookie, asks SessionStore to resolve it, and either populates req.ctx.session or responds 401. Every protected route in the app is mounted behind this function, so changing its behaviour changes auth globally.
show source
// src/middleware/auth.ts export async function verifyToken(req, res, next) { const raw = req.signedCookies['fw_sid']; if (!raw) return res.status(401).end(); const session = await SessionStore.get(raw); if (!session || session.expiresAt < Date.now()) { return res.status(401).end(); } req.ctx = { session }; next(); }
SessionStore is a small read-through cache: it checks an in-process LRU first, then falls back to Postgres. Writes (create, revoke) always go straight to the DB and invalidate the cache entry so other workers don't serve a stale session.
show source
// src/lib/sessionStore.ts const cache = new LRU<string, Session>({ max: 5000, ttl: 60_000 }); export const SessionStore = { async get(id: string) { const hit = cache.get(id); if (hit) return hit; const row = await db.one(SELECT_SESSION, [id]); if (row) cache.set(id, row); return row ?? null; }, /* create, revoke, touch ... */ };
The sessions table is keyed on a random 32-byte id (the cookie value) with a covering index on user_id for "sign out everywhere". Expiry is enforced both here (expires_at) and again in the middleware as defence in depth.
show source
-- db/migrations/004_sessions.sql create table sessions ( id text primary key, user_id uuid not null references users(id), created_at timestamptz default now(), expires_at timestamptz not null, ip inet, user_agent text ); create index sessions_user_id_idx on sessions(user_id);