Claude
Skills
Sign in
Back

bknd-session-handling

Included with Lifetime
$97 forever

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.

General

What this skill does


# 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: `bknd` package installed
- For React: `@bknd/react` package 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**:

1. **Login** - Server creates signed JWT with user data, returns token
2. **Storage** - Token stored in cookie (automatic) or localStorage/header (manual)
3. **Requests** - Token sent with each request for authentication
4. **Validation** - Server validates signature and expiration
5. **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

```typescript
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

```typescript
{
  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

```typescript
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

```typescript
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

```typescript
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)

```typescript
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)

```typescript
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

```typescript
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: R

Related in General