Claude-skill-registry bknd-session-handling
Use when managing user sessions in a Bknd application. Covers JWT token lifecycle, session persistence, automatic renewal, checking auth state, invalidating sessions, and handling expiration.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/bknd-session-handling" ~/.claude/skills/majiayu000-claude-skill-registry-bknd-session-handling && rm -rf "$T"
skills/data/bknd-session-handling/SKILL.mdSession Handling
Manage user sessions in Bknd: token persistence, session checking, auto-renewal, and invalidation.
Prerequisites
- Bknd project with auth enabled (
)bknd-setup-auth - Auth strategy configured and working (
)bknd-login-flow - For SDK:
package installedbknd - For React:
package installed@bknd/react
When to Use UI Mode
- Viewing JWT configuration in admin panel
- Checking cookie settings
- Testing session expiration
UI steps: Admin Panel > Auth > Configuration > JWT/Cookie settings
When to Use Code Mode
- Implementing session persistence in frontend
- Checking authentication state on page load
- Handling token expiration gracefully
- Implementing auto-refresh patterns
- Server-side session validation
How Sessions Work in Bknd
Bknd uses stateless JWT-based sessions:
- Login - Server creates signed JWT with user data, returns token
- Storage - Token stored in cookie (automatic) or localStorage/header (manual)
- Requests - Token sent with each request for authentication
- Validation - Server validates signature and expiration
- Renewal - Cookie can auto-renew; header tokens require manual refresh
Key Concept: No server-side session storage. Token itself is the session.
Session Configuration
JWT Settings
import { defineConfig } from "bknd"; export default defineConfig({ auth: { enabled: true, jwt: { secret: process.env.JWT_SECRET!, // Required for production alg: "HS256", // Algorithm: HS256 | HS384 | HS512 expires: 604800, // 7 days in seconds issuer: "my-app", // Token issuer claim fields: ["id", "email", "role"], // User fields in token payload }, }, });
JWT options:
| Option | Type | Default | Description |
|---|---|---|---|
| string | | Signing secret (256-bit min for production) |
| string | | HMAC algorithm |
| number | - | Token lifetime in seconds |
| string | - | Issuer claim (iss) |
| string[] | | User fields encoded in token |
Cookie Settings
{ auth: { cookie: { secure: process.env.NODE_ENV === "production", // HTTPS only httpOnly: true, // No JS access sameSite: "lax", // CSRF protection expires: 604800, // Match JWT expiry renew: true, // Auto-extend on activity path: "/", // Cookie scope pathSuccess: "/dashboard", // Redirect after login pathLoggedOut: "/login", // Redirect after logout }, }, }
Cookie options:
| Option | Type | Default | Description |
|---|---|---|---|
| boolean | | Require HTTPS |
| boolean | | Block JavaScript access |
| string | | | | |
| number | | Cookie lifetime (seconds) |
| boolean | | Auto-renew on requests |
| string | | Post-login redirect |
| string | | Post-logout redirect |
SDK Approach
Session Persistence with Storage
import { Api } from "bknd"; // Persistent sessions (survives page refresh/browser restart) const api = new Api({ host: "http://localhost:7654", storage: localStorage, // Token persisted }); // Session-only (cleared when tab closes) const api = new Api({ host: "http://localhost:7654", storage: sessionStorage, // Token cleared on tab close }); // No persistence (token in memory only) const api = new Api({ host: "http://localhost:7654", // No storage = token lost on page refresh });
Check Session on App Start
async function initializeAuth() { const api = new Api({ host: "http://localhost:7654", storage: localStorage, }); // Check if existing token is still valid const { ok, data } = await api.auth.me(); if (ok && data?.user) { console.log("Session valid:", data.user.email); return { api, user: data.user }; } console.log("No valid session"); return { api, user: null }; } // On app mount const { api, user } = await initializeAuth();
Session State Management
import { Api } from "bknd"; class SessionManager { private api: Api; private user: User | null = null; private listeners: Set<(user: User | null) => void> = new Set(); constructor(host: string) { this.api = new Api({ host, storage: localStorage }); } // Initialize - call on app start async init() { const { ok, data } = await this.api.auth.me(); this.user = ok ? data?.user ?? null : null; this.notifyListeners(); return this.user; } // Get current session getUser() { return this.user; } isAuthenticated() { return this.user !== null; } // Login - creates new session async login(email: string, password: string) { const { ok, data, error } = await this.api.auth.login("password", { email, password, }); if (!ok) throw new Error(error?.message || "Login failed"); this.user = data!.user; this.notifyListeners(); return this.user; } // Logout - destroys session async logout() { await this.api.auth.logout(); this.user = null; this.notifyListeners(); } // Refresh session (re-validate token) async refresh() { const { ok, data } = await this.api.auth.me(); this.user = ok ? data?.user ?? null : null; this.notifyListeners(); return this.user; } // Subscribe to session changes subscribe(callback: (user: User | null) => void) { this.listeners.add(callback); return () => this.listeners.delete(callback); } private notifyListeners() { this.listeners.forEach((cb) => cb(this.user)); } } type User = { id: number; email: string; role?: string }; // Usage const session = new SessionManager("http://localhost:7654"); await session.init(); session.subscribe((user) => { console.log("Session changed:", user?.email || "logged out"); });
Cookie-Based Sessions (Automatic)
const api = new Api({ host: "http://localhost:7654", tokenTransport: "cookie", // Use httpOnly cookies }); // Login sets cookie automatically await api.auth.login("password", { email, password }); // All requests include cookie automatically await api.data.readMany("posts"); // Logout clears cookie await api.auth.logout();
Cookie mode advantages:
- HttpOnly = XSS protection (JavaScript can't access token)
- Auto-renewal on every request (if
)cookie.renew: true - No manual token management
- Automatic CSRF protection with
sameSite
Header-Based Sessions (Manual)
const api = new Api({ host: "http://localhost:7654", storage: localStorage, tokenTransport: "header", // Default }); // Token stored in localStorage, sent via Authorization header await api.auth.login("password", { email, password }); // Token automatically included: // Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Handling Session Expiration
Detect Expired Token
async function makeAuthenticatedRequest<T>(fn: () => Promise<T>): Promise<T> { try { return await fn(); } catch (error) { // Check if error is due to expired session if (isAuthError(error)) { // Session expired - redirect to login or refresh await handleExpiredSession(); } throw error; } } function isAuthError(error: unknown): boolean { if (error instanceof Error) { return error.message.includes("401") || error.message.includes("Unauthorized"); } return false; } async function handleExpiredSession() { // Option 1: Redirect to login window.location.href = "/login?expired=true"; // Option 2: Show re-authentication modal // showReauthModal(); // Option 3: Try to refresh (if using refresh tokens) // await refreshToken(); }
Auto-Refresh Pattern
Since Bknd uses stateless JWT, there's no built-in refresh token. Instead, use
api.auth.me() to re-validate and extend cookie-based sessions:
class SessionWithAutoRefresh { private api: Api; private refreshInterval: number | null = null; constructor(host: string) { this.api = new Api({ host, tokenTransport: "cookie", // Cookie auto-renews on requests }); } // Start periodic session check startAutoRefresh(intervalMs = 5 * 60 * 1000) { // Every 5 minutes this.refreshInterval = window.setInterval(async () => { const { ok } = await this.api.auth.me(); if (!ok) { this.stopAutoRefresh(); this.onSessionExpired(); } }, intervalMs); } stopAutoRefresh() { if (this.refreshInterval) { clearInterval(this.refreshInterval); this.refreshInterval = null; } } private onSessionExpired() { // Handle expired session window.location.href = "/login?session=expired"; } }
Proactive Token Refresh
For header-based auth, re-login before token expires:
import { jwtDecode } from "jwt-decode"; // npm install jwt-decode class TokenManager { private api: Api; private refreshTimer: number | null = null; constructor(host: string) { this.api = new Api({ host, storage: localStorage }); } // Schedule refresh before expiry scheduleRefresh(token: string) { const decoded = jwtDecode<{ exp: number }>(token); const expiresAt = decoded.exp * 1000; // Convert to ms const refreshAt = expiresAt - 5 * 60 * 1000; // 5 min before expiry const delay = refreshAt - Date.now(); if (delay > 0) { this.refreshTimer = window.setTimeout(() => { this.promptRelogin(); }, delay); } } private promptRelogin() { // Show modal asking user to re-authenticate // Or redirect to login with return URL } cleanup() { if (this.refreshTimer) { clearTimeout(this.refreshTimer); } } }
React Integration
Session Provider
import { createContext, useContext, useEffect, useState, ReactNode } from "react"; import { Api } from "bknd"; type User = { id: number; email: string; role?: string }; type SessionContextType = { user: User | null; isLoading: boolean; isAuthenticated: boolean; checkSession: () => Promise<User | null>; clearSession: () => void; }; const SessionContext = createContext<SessionContextType | null>(null); const api = new Api({ host: "http://localhost:7654", storage: localStorage, }); export function SessionProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState<User | null>(null); const [isLoading, setIsLoading] = useState(true); // Check session on mount useEffect(() => { checkSession().finally(() => setIsLoading(false)); }, []); async function checkSession() { const { ok, data } = await api.auth.me(); const user = ok ? data?.user ?? null : null; setUser(user); return user; } function clearSession() { setUser(null); api.auth.logout(); } return ( <SessionContext.Provider value={{ user, isLoading, isAuthenticated: user !== null, checkSession, clearSession, }} > {children} </SessionContext.Provider> ); } export function useSession() { const context = useContext(SessionContext); if (!context) throw new Error("useSession must be used within SessionProvider"); return context; }
Session-Aware Components
import { useSession } from "./SessionProvider"; function Header() { const { user, isAuthenticated, clearSession } = useSession(); if (!isAuthenticated) { return <a href="/login">Login</a>; } return ( <div> <span>Welcome, {user!.email}</span> <button onClick={clearSession}>Logout</button> </div> ); } function ProtectedPage() { const { isLoading, isAuthenticated } = useSession(); if (isLoading) return <div>Checking session...</div>; if (!isAuthenticated) return <Navigate to="/login" />; return <div>Protected content</div>; }
Session Expiration Handler
import { useEffect } from "react"; import { useSession } from "./SessionProvider"; function SessionExpirationHandler() { const { checkSession, clearSession } = useSession(); useEffect(() => { // Check session periodically const interval = setInterval(async () => { const user = await checkSession(); if (!user) { // Session expired alert("Your session has expired. Please log in again."); clearSession(); window.location.href = "/login"; } }, 5 * 60 * 1000); // Every 5 minutes // Check on window focus (user returns to tab) const handleFocus = () => checkSession(); window.addEventListener("focus", handleFocus); return () => { clearInterval(interval); window.removeEventListener("focus", handleFocus); }; }, [checkSession, clearSession]); return null; // Invisible component } // Add to app root function App() { return ( <SessionProvider> <SessionExpirationHandler /> <Routes /> </SessionProvider> ); }
Server-Side Session Validation
Validate Session in API Routes
import { getApi } from "bknd"; export async function GET(request: Request, app: BkndApp) { const api = getApi(app); const user = await api.auth.resolveAuthFromRequest(request); if (!user) { return new Response("Unauthorized", { status: 401 }); } // Session valid - user data available console.log("User ID:", user.id); console.log("Email:", user.email); console.log("Role:", user.role); return new Response(JSON.stringify({ user })); }
Server-Side Session Check (Next.js)
// app/api/me/route.ts import { getApp, getApi } from "bknd/adapter/nextjs"; export async function GET(request: Request) { const app = await getApp(); const api = getApi(app); const user = await api.auth.resolveAuthFromRequest(request); if (!user) { return Response.json({ user: null }, { status: 401 }); } return Response.json({ user }); }
Common Patterns
Remember Last Activity
// Track user activity for session timeout warnings let lastActivity = Date.now(); // Update on user interaction document.addEventListener("click", () => (lastActivity = Date.now())); document.addEventListener("keypress", () => (lastActivity = Date.now())); // Check for inactivity setInterval(() => { const inactiveMinutes = (Date.now() - lastActivity) / 1000 / 60; if (inactiveMinutes > 25) { // Warn user session will expire soon showSessionWarning(); } if (inactiveMinutes > 30) { // Force logout api.auth.logout(); window.location.href = "/login?reason=inactive"; } }, 60000); // Check every minute
Multi-Tab Session Sync
// Sync session state across browser tabs window.addEventListener("storage", async (event) => { if (event.key === "auth") { if (event.newValue === null) { // Logged out in another tab window.location.href = "/login"; } else { // Logged in in another tab - refresh session await api.auth.me(); window.location.reload(); } } });
Secure Session Storage
// For sensitive apps, use sessionStorage + warn on tab close const api = new Api({ host: "http://localhost:7654", storage: sessionStorage, }); window.addEventListener("beforeunload", (e) => { if (api.auth.me()) { e.preventDefault(); e.returnValue = "You will be logged out if you leave."; } });
Common Pitfalls
Session Lost on Refresh
Problem: User logged out after page refresh
Fix: Provide storage adapter:
// Wrong - no persistence const api = new Api({ host: "http://localhost:7654" }); // Correct const api = new Api({ host: "http://localhost:7654", storage: localStorage, });
Cookie Not Working Locally
Problem: Cookie not set in development
Fix: Disable secure flag for localhost:
{ auth: { cookie: { secure: process.env.NODE_ENV === "production", // false in dev }, }, }
Session Check Blocking UI
Problem: App shows blank while checking session
Fix: Show loading state:
function App() { const { isLoading } = useSession(); if (isLoading) { return <LoadingSpinner />; // Don't leave blank } return <Routes />; }
Expired Token Still in Storage
Problem: Old token causes continuous 401 errors
Fix: Clear storage on auth failure:
async function checkSession() { const { ok } = await api.auth.me(); if (!ok) { // Clear stale token localStorage.removeItem("auth"); return null; } return user; }
Verification
Test session handling:
1. Session persists across refresh:
// Login await api.auth.login("password", { email: "test@example.com", password: "pass" }); // Refresh page, then: const { ok, data } = await api.auth.me(); console.log("Session persists:", ok && data?.user); // Should be true
2. Session expires correctly:
// Set short expiry in config (for testing) jwt: { expires: 10 } // 10 seconds // Login, wait 15 seconds await api.auth.login("password", { email, password }); await new Promise(r => setTimeout(r, 15000)); const { ok } = await api.auth.me(); console.log("Session expired:", !ok); // Should be true
3. Logout clears session:
await api.auth.logout(); const { ok } = await api.auth.me(); console.log("Session cleared:", !ok); // Should be true
DOs and DON'Ts
DO:
- Configure appropriate JWT expiry for your use case
- Use httpOnly cookies when possible (XSS protection)
- Check session validity on app initialization
- Handle session expiration gracefully with UI feedback
- Match cookie expiry with JWT expiry
- Use
in productionsecure: true
DON'T:
- Store tokens in memory only (lost on refresh)
- Use long expiry times without renewal mechanism
- Ignore session expiration errors
- Mix cookie and header auth without clear reason
- Disable httpOnly unless absolutely necessary
- Forget to clear storage on logout
Related Skills
- bknd-setup-auth - Configure authentication system
- bknd-login-flow - Login/logout functionality
- bknd-oauth-setup - OAuth/social login providers
- bknd-protect-endpoint - Secure specific endpoints
- bknd-public-vs-auth - Configure public vs authenticated access