
bknd-session-handling
by cameronapak
A no-build, un-bloated stack built upon Web Standards that feels freeing to use and can be deployed anywhere.
SKILL.md
name: bknd-session-handling description: 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.
Session 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:
bkndpackage installed - For React:
@bknd/reactpackage installed
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 |
|---|---|---|---|
secret | string | "" | Signing secret (256-bit min for production) |
alg | string | "HS256" | HMAC algorithm |
expires | number | - | Token lifetime in seconds |
issuer | string | - | Issuer claim (iss) |
fields | string[] | ["id","email","role"] | 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 |
|---|---|---|---|
secure | boolean | true | Require HTTPS |
httpOnly | boolean | true | Block JavaScript access |
sameSite | string | "lax" | "strict" | "lax" | "none" |
expires | number | 604800 | Cookie lifetime (seconds) |
renew | boolean | true | Auto-renew on requests |
pathSuccess | string | "/" | Post-login redirect |
pathLoggedOut | 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
secure: truein production
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
Score
Total Score
Based on repository quality metrics
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
1ヶ月以内に更新
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
Reviews
Reviews coming soon
