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.
npx claudepluginhub cameronapak/bknd-expert --plugin bknd-research-skillsThis skill uses the workspace's default tool permissions.
Manage user sessions in Bknd: token persistence, session checking, auto-renewal, and invalidation.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Checks Next.js compilation errors using a running Turbopack dev server after code edits. Fixes actionable issues before reporting complete. Replaces `next build`.
Manage user sessions in Bknd: token persistence, session checking, auto-renewal, and invalidation.
bknd-setup-auth)bknd-login-flow)bknd package installed@bknd/react package installedUI steps: Admin Panel > Auth > Configuration > JWT/Cookie settings
Bknd uses stateless JWT-based sessions:
Key Concept: No server-side session storage. Token itself is the session.
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 |
{
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 |
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
});
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();
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");
});
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:
cookie.renew: true)sameSiteconst 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...
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();
}
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";
}
}
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);
}
}
}
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;
}
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>;
}
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>
);
}
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 }));
}
// 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 });
}
// 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
// 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();
}
}
});
// 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.";
}
});
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,
});
Problem: Cookie not set in development
Fix: Disable secure flag for localhost:
{
auth: {
cookie: {
secure: process.env.NODE_ENV === "production", // false in dev
},
},
}
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 />;
}
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;
}
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
DO:
secure: true in productionDON'T: