Package Exports
- @wristband/typescript-session
Readme
Enterprise-ready auth that is secure by default, truly multi-tenant, and ungated for small businesses.
Wristband TypeScript Session SDK
Secure, encrypted cookie-based session management for TypeScript applications.
Overview
This SDK provides enterprise-grade session management with:
- 🎯 Framework Agnostic - Express, Next.js, Remix, and more
- 🌐 Universal Runtime Support - Node.js, Cloudflare Workers, Vercel Edge, and more
- 📦 Zero Dependencies - Pure TypeScript with web standards
- 🔒 Secure Encryption - AES-256-GCM encryption with Web Crypto API
- 🔄 Secret Key Rotation - Seamless secret rotation without invalidating sessions
- ⚡ High Performance - Deferred mode prevents redundant encryption in Node.js
- 🛡️ CSRF Protection - Optional token-based CSRF protection
Use it standalone for any TypeScript application, or pair with Wristband for a complete multi-tenant authentication solution.
Cookie-Based Sessions Explained
This SDK uses encrypted cookie-based sessions - a lightweight approach where session data is stored in the browser as an encrypted cookie rather than on the server.
How It Works
This SDK uses encryption (AES-256-GCM) rather than just signing (like signed JWTs) to keep your session data completely private.
- Server encrypts session data using AES-256-GCM
- Browser stores the encrypted cookie (max 4KB)
- Server decrypts the cookie on each request
- No database required - session data lives in the cookie
Why Use Cookie-Based Sessions?
Perfect for modern web applications:
- ✅ Zero infrastructure - No Redis or databases required
- ✅ Scales effortlessly - No session state to sync across servers
- ✅ Edge-compatible - Works in serverless, Workers, and Edge runtimes
- ✅ Low latency - No database lookup on every request
When NOT to Use Cookie-Based Sessions
Consider server-side sessions (Redis, database) if you need:
- Large session data (>3KB) - Store shopping carts with 100+ items, extensive user profiles
- Instant cross-device logout - Admin needs to immediately revoke all of a user's sessions
The 4KB Limit
Browser cookies are limited to 4KB total (per RFC 6265). After encryption overhead and cookie attributes, you have ~3KB for actual session data.
What fits in ~3KB:
// ✅ This fits comfortably
{
userId: "user_abc123",
tenantId: "tenant_xyz789",
accessToken: "eyJhbGc...", // JWT (~500-1000 bytes)
email: "user@example.com",
role: "admin",
preferences: { theme: "dark", language: "en" },
lastLogin: 1735689600000
}What doesn't fit:
// ❌ Too large
{
shoppingCart: [...100 items...], // Too much data
userHistory: [...months of activity...], // Too much data
profileData: { ...extensive user info...} // Too much data
}Solution for large data: Store a reference ID in the session, fetch full data from database when needed:
// ✅ Store reference in session
session.cartId = "cart_abc123";
// ✅ Fetch full cart from database when needed
const cart = await db.getCart(session.cartId);Table of Contents
- Installation
- Basic Usage
- Platform Examples
- API Reference
- Error Handling
- Advanced Topics
- Wristband Integration
- Using with Wristband Auth SDKs
- Questions
Installation
# With npm
npm install @wristband/typescript-session
# Or with yarn
yarn add @wristband/typescript-session
# Or with pnpm
pnpm add @wristband/typescript-sessionBasic Usage
Using with Wristband?
See Wristband Integration for auth-specific examples.
How it works:
- Call
getSession()to read the encrypted cookie from the request (returns either existing session data or an empty session if no cookie exists yet) - Read/write session data using the session object
- Call
save()ordestroy()to persist changes - Your application handles what to do based on whether the session contains data or not
Node.js / Express
For Node.js, pass request, response, and session options to getSession(). The SDK will write cookies directly to the response object.
1: Add session middleware
Create a middleware that adds a session to every request:
// src/app.ts (Express)
import express from 'express';
import { getSession } from '@wristband/typescript-session';
const app = express();
// Add session to all requests
app.use(async (req, res, next) => {
req.session = await getSession(req, res, {
secrets: 'your-secret-key-min-32-characters-long',
maxAge: 86400, // 24 hours
});
next();
});2: Use sessions in your routes
Save session data when users log in:
app.post('/login', async (req, res) => {
// Authenticate user (your logic here)
// ...
// Store user data in session
req.session.userId = '123';
req.session.loginTime = Date.now();
await req.session.save(); // Encrypt and write cookie
res.json({ success: true });
});Destroy session when users log out:
app.post('/logout', async (req, res) => {
req.session.destroy(); // Destroy cookie
res.json({ success: true });
});💡 Performance Tip
For Express applications, consider using
getSessionSync()with deferred mode to batch writes. See Deferred Mode for details.
Edge Runtimes (Next.js, Cloudflare Workers)
For Edge runtimes, pass request and session options to getSession(). Instead of save() and destroy(), you'll use saveToResponse() or destroyToResponse() to clone your Response and append session cookies.
Step 1: Create a login endpoint
In Edge runtimes, you don't pass a response object to getSession(). Instead, create your Response and pass it to saveToResponse(), which clones it and adds the session cookie:
// app/api/login/route.ts (Next.js App Router)
import { getSession } from '@wristband/typescript-session';
export async function POST(request: Request) {
// Get session from request only (no response object)
const session = await getSession(request, {
secrets: 'your-secret-key-min-32-characters-long',
maxAge: 86400, // 24 hours
});
// Authenticate user (your logic here)
// ...
// Store user data in session
session.userId = '123';
session.loginTime = Date.now();
// Return new Response with session cookie
const response = Response.json({ success: true });
return await session.saveToResponse(response);
}Step 2: Create a logout endpoint
Use destroyToResponse() to destroy the session and return a Response with the deletion cookie:
// app/api/logout/route.ts (Next.js App Router)
import { getSession } from '@wristband/typescript-session';
export async function POST(request: Request) {
const session = await getSession(request, {
secrets: 'your-secret-key-min-32-characters-long',
maxAge: 86400, // 24 hours
});
// Destroy session and return response with deletion cookie
const response = Response.json({ success: true });
return await session.destroyToResponse(response);
}Key Differences:
Node.js:
- Pass
request,response, and config options togetSession() - Use
save()to write cookies (mutates response object) - Use
destroy()to delete cookies (mutates response object) - Cookies are written directly to the response via
res.setHeader()
Edge Runtimes:
- Pass only
requestand config options togetSession()(no response) - Use
saveToResponse(response)to clone and add cookies - Use
destroyToResponse(response)to clone and add deletion cookies - Returns a new Response object with cookies appended to headers
💡 Generating Secure Secrets
Your session secret must be at least 32 characters for security. Generate one using:
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"Best practice: Store secrets in environment variables, never commit them to source control.
Platform Examples
Express (Node.js)
Basic setup with middleware:
import express from 'express';
import { getSession } from '@wristband/typescript-session';
const app = express();
// Session middleware
app.use(async (req, res, next) => {
req.session = await getSession(req, res, {
secrets: 'your-secret-key-min-32-characters-long',
maxAge: 86400,
});
next();
});
// Login route
app.post('/login', async (req, res) => {
// Your auth logic...
const user = await authenticateUser(req.body.email, req.body.password);
req.session.userId = user.id;
req.session.email = user.email;
await req.session.save();
res.json({ success: true });
});
// Protected route
app.get('/dashboard', async (req, res) => {
if (!req.session.userId) {
return res.redirect('/login');
}
res.render('dashboard', { user: req.session });
});
// Logout route
app.post('/logout', async (req, res) => {
req.session.destroy();
res.json({ success: true });
});Performance Optimization
For production Express apps, consider using
getSessionSync()with deferred mode to batch session writes. See Deferred Mode for details.
Next.js
App Router - Route Handlers
// app/api/login/route.ts
import { getSession } from '@wristband/typescript-session';
export async function POST(request: Request) {
const session = await getSession(request, {
secrets: 'your-secret-key-min-32-characters-long',
maxAge: 86400,
});
// Your auth logic
const { email, password } = await request.json();
const user = await authenticateUser(email, password);
// Save to session
session.userId = user.id;
session.email = user.email;
const response = Response.json({ success: true });
return await session.saveToResponse(response);
}// app/api/logout/route.ts
import { getSession } from '@wristband/typescript-session';
export async function POST(request: Request) {
const session = await getSession(request, {
secrets: 'your-secret-key-min-32-characters-long',
maxAge: 86400,
});
const response = Response.json({ success: true });
return session.destroyToResponse(response);
}Middleware (Route Protection):
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { getSession } from '@wristband/typescript-session';
export async function middleware(request: NextRequest) {
const session = await getSession(request, {
secrets: 'your-secret-key-min-32-characters-long',
maxAge: 86400,
});
// Protect dashboard routes
if (!session.userId && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: '/dashboard/:path*',
};Server Actions:
Server Actions can't return Response objects, so you need to manually manage cookies via Next.js's cookies() API. You can build helper functions that use getCookieDataForSave() and getCookieDataForDestroy():
// lib/session-helpers.ts
import { cookies } from 'next/headers';
import {
getSession,
Session,
SessionOptions,
SessionData
} from '@wristband/typescript-session';
// Next.js cookie store interface (duck-typed)
interface NextJsCookieStore {
get(name: string): { value: string } | undefined;
set(name: string, value: string, options?: any): void;
}
// Get session for Server Actions
export async function getSessionForServer<T extends SessionData = SessionData>(
cookieStore: NextJsCookieStore,
options: SessionOptions
): Promise<Session<T> & T> {
const cookieName = options.cookieName || 'session'; // SDK Default
const cookieValue = cookieStore.get(cookieName)?.value;
// Create a real Web Request with the session cookie
const request = new Request('https://placeholder.local', {
headers: { cookie: cookieValue ? `${cookieName}=${cookieValue}` : '' },
});
return await getSession<T>(request, options);
}
// Save session for Server Actions
export async function saveSessionForServer<T extends SessionData = SessionData>(
cookieStore: NextJsCookieStore,
session: Session<T> & T
): Promise<void> {
const cookieData = await session.getCookieDataForSave();
for (const { name, value, options } of cookieData) {
cookieStore.set(name, value, options);
}
}
// Destroy session for Server Actions
export function destroySessionForServer<T extends SessionData = SessionData>(
cookieStore: NextJsCookieStore,
session: Session<T> & T
): void {
const cookieData = session.getCookieDataForDestroy();
for (const { name, value, options } of cookieData) {
cookieStore.set(name, value, options);
}
}Then use the helpers in your Server Actions:
// app/actions.ts
'use server';
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
import {
destroySessionForServer,
getSessionForServer,
saveSessionForServer
} from '@/lib/session';
const sessionOptions = {
secrets: 'your-secret-key-min-32-characters-long',
maxAge: 86400,
};
export async function loginAction(formData: FormData) {
const cookieStore = await cookies();
const session = await getSessionForServer(cookieStore, sessionOptions);
// Your auth logic
const email = formData.get('email') as string;
const password = formData.get('password') as string;
const user = await authenticateUser(email, password);
// Save user to session
session.userId = user.id;
session.email = user.email;
await saveSessionForServer(cookieStore, session);
redirect('/dashboard');
}
export async function updatePreferences(theme: string) {
const cookieStore = await cookies();
const session = await getSessionForServer(cookieStore, sessionOptions);
if (!session.userId) {
redirect('/login');
}
// Update session
session.preferences = { theme };
await saveSessionForServer(cookieStore, session);
}
export async function logoutAction() {
const cookieStore = await cookies();
const session = await getSessionForServer(cookieStore, sessionOptions);
destroySessionForServer(cookieStore, session);
redirect('/login');
}Cloudflare Workers
export default {
async fetch(request: Request): Promise<Response> {
const session = await getSession(request, {
secrets: 'your-secret-key-min-32-characters-long',
maxAge: 24 * 60 * 60, // 24 hours
});
const url = new URL(request.url);
if (url.pathname === '/login' && request.method === 'POST') {
// Your auth logic
const { email, password } = await request.json();
const user = await authenticateUser(email, password);
// Save to session
session.userId = user.id;
session.email = user.email;
const response = Response.json({ success: true });
return await session.saveToResponse(response);
}
if (url.pathname === '/dashboard') {
if (!session.userId) {
return Response.redirect(new URL('/login', request.url));
}
return Response.json({
message: 'Welcome to dashboard',
user: { id: session.userId, email: session.email },
});
}
if (url.pathname === '/logout' && request.method === 'POST') {
const response = Response.json({ success: true });
return session.destroyToResponse(response);
}
return Response.json({ error: 'Not found' }, { status: 404 });
},
};💡 Learn More
For more documentation on
getCookieDataForSave()andgetCookieDataForDestroy(), see the Next.js Server Actions Methods section in the API Reference.
API Reference
getSession()
Retrieves an existing session or creates a new empty session. This is the primary function for session management.
Signature:
// Node.js (with response object)
function getSession<T extends SessionData = SessionData>(
request: IncomingMessage | Request,
response: ServerResponse | Response,
options: SessionOptions
): Promise<Session<T> & T>
// Edge Runtimes (without response object)
function getSession<T extends SessionData = SessionData>(
request: Request,
options: SessionOptions
): Promise<Session<T> & T>Parameters:
request- The incoming HTTP request (Node.jsIncomingMessage,NextApiRequest, ExpressRequest, or WebRequest)response- The HTTP response object (Node.js only; not needed in Edge runtimes)options- Session configuration (see SessionOptions)
Returns:
Promise<Session<T> & T> - A promise resolving to a session instance that provides both session management methods and direct access to the underlying data.
Throws:
SessionError if configuration is invalid or request type is unsupported
Example:
// Node.js / Express
const session = await getSession(req, res, {
secrets: 'your-secret-key-min-32-characters-long',
maxAge: 86400,
});
// Edge Runtime (Next.js, Cloudflare Workers, etc.)
const session = await getSession(request, {
secrets: 'your-secret-key-min-32-characters-long',
maxAge: 86400,
});getSessionSync()
Synchronous version of getSession() for Node.js environments. Used primarily with deferred mode in Express for better performance.
Signature:
function getSessionSync<T extends SessionData = SessionData>(
request: IncomingMessage | Request,
response: ServerResponse | Response,
options: SessionOptions
): Session<T> & TParameters:
Same as getSession()
Returns:
Session<T> & T - A session instance that provides both session management methods and direct access to the underlying data.
Example:
import { getSessionSync } from '@wristband/typescript-session';
app.use((req, res, next) => {
req.session = getSessionSync(req, res, {
secrets: 'your-secret-key-min-32-characters-long',
maxAge: 86400,
});
req.session.enableDeferredMode();
const prevWriteHead = res.writeHead.bind(res);
res.writeHead = function(...args) {
if (!res.headersSent) {
req.session.flushSync();
}
return prevWriteHead(...args);
};
next();
});See Deferred Mode for more details.
SessionOptions
These SDK configuration options control how sessions are created, encrypted, and stored in cookies. All cookie-related options follow standard HTTP cookie specifications.
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
secrets |
string or string[] |
Yes | N/A | Secret key(s) used for encrypting and decrypting session cookie data. Single string: Used for both encryption and decryption. Array of strings: First key encrypts new sessions, all keys tried for decryption (enables key rotation). Example: [process.env.NEW_SECRET, process.env.OLD_SECRET] Secrets must be at least 32 characters for security. Use cryptographically random strings (e.g., from a secure password generator). |
cookieName |
string |
No | 'session' |
The name of the session cookie to set in the response. |
maxAge |
number |
No | 3600 (1 hour) |
Session expiration time in seconds. Determines how long a session remains valid. After this time, the session cookie expires and users must re-authenticate. Example: 86400 for 24 hours, 604800 for 7 days. |
path |
string |
No | '/' |
Cookie path attribute. Specifies which URL paths can access the cookie. Default '/' means all paths. |
secure |
boolean |
No | true |
Cookie Secure flag. If true, cookie is only sent over HTTPS connections. Security recommendation: Always use true in production. Only set to false for local development over HTTP. |
sameSite |
'Strict', 'Lax', or 'None' |
No | 'Lax' |
Cookie SameSite setting. Controls whether cookies are sent on cross-site requests. 'Strict': Cookies only sent for same-site requests (recommended, when possible). 'Lax': Cookies sent for same-site requests plus top-level navigation GET requests (good balance). 'None': Cookies sent for all requests (requires secure: true; use with CSRF token protection enabled; use only if you know what you are doing). |
domain |
string |
No | undefined |
Cookie domain attribute. Controls which domains can access the cookie. undefined: Cookie only sent to current domain. '.example.com': Cookie sent to example.com and all subdomains. 'app.example.com': Cookie only sent to app.example.com. |
enableCsrfProtection |
boolean |
No | false |
Enable CSRF protection by generating and validating CSRF tokens. Recommendation: Only enable if you use sameSite: 'None' or want defense-in-depth. The default sameSite: 'Lax' (or 'Strict') provides sufficient CSRF protection for most cases. When enabled: CSRF token is automatically generated after authentication (via any save() function), stored in both session and a separate cookie, and must be included in request headers for server validation. When disabled: No CSRF tokens or cookies are generated. |
csrfCookieName |
string |
No | 'CSRF-TOKEN' |
Name of the CSRF cookie. Only used if enableCsrfProtection is true. |
csrfCookieDomain |
string |
No | undefined |
Domain for CSRF cookie. Follows same rules as domain option. Falls back to domain value if not specified. Only used if enableCsrfProtection is true. |
Direct Session Data Access
The session object supports direct property access for reading and writing data. Direct access is fully type-safe when using TypeScript with a custom session data type.
// Reading data
const userId = session.userId; // Same as session.get('userId')
const theme = session.theme; // Same as session.get('theme')
// Writing data
session.userId = '123'; // Same as session.set('userId', '123')
session.theme = 'dark'; // Same as session.set('theme', 'dark')
// Deleting data
delete session.theme; // Same as session.delete('theme')You can also use any of the methods below to update your session data.
Session Methods
Once you have a session object, you can use these methods:
Core Methods (All Runtimes)
get(key)
Type-safe getter for session data.
const userId = session.get('userId'); // string | undefined
const theme = session.get('theme'); // 'light' | 'dark' | undefinedParameters:
key: keyof T- The session data key
Returns:
T[key] | undefined
set(key, value)
Type-safe setter for session data.
session.set('userId', '123');
session.set('theme', 'dark');
session.set('lastVisit', Date.now());Parameters:
key: keyof T- The session data keyvalue: T[key]- The value to set
Returns:
void
Throws:
SessionError if session has been destroyed
delete(key)
Delete a key from session data.
session.delete('theme');
session.delete('lastVisit');Parameters:
key: keyof T- The session data key to delete
Returns:
void
Throws:
SessionError if session has been destroyed
toJSON()
Returns the session data as a plain object. This method is automatically called by JSON.stringify() and enables clean serialization of the session.
const sessionData = session.toJSON();
console.log(sessionData); // { userId: '123', theme: 'dark', ... }
// Also called automatically by JSON.stringify()
const json = JSON.stringify(session);
console.log(json); // '{"userId":"123","theme":"dark",...}'Parameters:
None
Returns:
T - A plain object containing all session data fields
Node.js Methods
These methods work in Node.js environments (Express, Next.js Pages Router, etc.):
save()
Encrypts session data and writes the cookie (mutates the response object). If enableCsrfProtection is true in the SDK configuration, a CSRF token is generated, stored in session.csrfToken, and written to the CSRF cookie.
💡 For Edge runtimes, use
saveToResponse().
await session.save();Returns:
Promise<void>
Throws:
SessionError if:
- Session has been destroyed
- No response object available
- Encryption fails
destroy()
Deletes the underlying session data as well as the session cookie from the response (mutates the response object). If enableCsrfProtection is true in the SDK configuration, then the CSRF cookie is deleted as well.
💡 For Edge runtimes, use
destroyToResponse().
session.destroy();Returns:
void
enableDeferredMode()
Enables deferred mode to batch cookie writes. Call this before any save() operations, then use flush() or flushSync() to write cookies at the end of the request.
See Deferred Mode for more details.
session.enableDeferredMode();
session.set('userId', '123');
await session.save(); // Marks for save (doesn't write yet)
await session.flush(); // Actually writes cookiesReturns:
void
flush()
Asynchronously flush pending session changes. Used with deferred mode.
await session.flush();Returns:
Promise<void>
Throws:
SessionError if:
- Deferred mode is not enabled
- No response object available
- Encryption fails
flushSync()
Synchronously flush pending session changes using Node.js crypto. Used with deferred mode.
session.flushSync();Returns:
void
Throws:
SessionError if:
- Deferred mode is not enabled
- No response object available
- Encryption fails
- Node.js crypto is not available
Edge Runtime Methods
These methods work in Edge runtimes (Cloudflare Workers, Vercel Edge, Deno Deploy, etc.):
saveToResponse(response)
Encrypts session data and returns a new Response with the session cookie appended. If enableCsrfProtection is true in the SDK configuration, a CSRF token is generated, stored in session.csrfToken, and written to the CSRF cookie.
💡 For Node.js runtimes, use
save().
session.userId = '123';
const response = new Response('Success');
return await session.saveToResponse(response);Parameters:
response: Response- The response to clone and append cookies to
Returns:
Promise<Response> - A Promise that resolves to a new Response object with session cookies
destroyToResponse(response)
Destroys the session and returns a new Response with deletion cookies appended. If enableCsrfProtection is true in the SDK configuration, then the CSRF cookie is deleted as well.
💡 For Node.js runtimes, use
destroy().
const response = new Response('Logged out');
return session.destroyToResponse(response);Parameters:
response: Response- The response to clone and append deletion cookies to
Returns:
Response - A new Response object with deletion cookies
Next.js Server Actions Methods
These are low-level methods for framework adapters that require manual cookie management. Next.js Server Actions can't return Response objects, so you can't use saveToResponse() or destroyToResponse(). Instead, these methods return cookie data that can be set via Next.js's cookies() API.
💡 Looking for examples? See the Next.js Platform Examples section for complete helper patterns and usage examples with Server Actions.
getCookieDataForSave()
Returns prepared cookie data for manual cookie setting (e.g., Next.js cookies().set()). If enableCsrfProtection is true, it will automatically generate a CSRF token and include the CSRF cookie data.
'use server';
import { cookies } from 'next/headers';
export async function loginAction() {
const session = await getSession(/* ... */);
session.set('userId', '123');
const cookieData = await session.getCookieDataForSave();
const cookieStore = await cookies();
for (const { name, value, options } of cookieData) {
cookieStore.set(name, value, options);
}
}Returns:
Promise<Array<{ name: string; value: string; options: CookieOptions }>> -
A Promise that resolves to an array of cookie objects, where each object contains:
name (string): The cookie name (e.g.,
'session'or'CSRF-TOKEN')value (string): The encrypted session data (for session cookie) or CSRF token value (for CSRF cookie)
options (
CookieOptions): Cookie configuration object with the following properties:- maxAge (number): Cookie expiration time in seconds
- domain (string | undefined): Cookie domain attribute
- path (string): Cookie path attribute (default: '/')
- secure (boolean): Requires HTTPS when true (default: true)
- httpOnly (boolean): Prevents JavaScript access when true (default: true)
- sameSite ('Strict' | 'Lax' | 'None'): Controls cross-site request behavior (default: 'Lax')
The returned array will contain:
- Always: 1 session cookie object
- Conditionally: 1 CSRF cookie object (if
enableCsrfProtectionistrue)
Throws:
SessionError if:
- Session has been destroyed
- Encryption fails
getCookieDataForDestroy()
Returns prepared cookie deletion data for manual cookie deletion. Returns cookies with empty values and expiration dates in the past. If enableCsrfProtection is true, includes CSRF cookie deletion data.
const cookieData = session.getCookieDataForDestroy();
const cookieStore = await cookies();
for (const { name, value, options } of cookieData) {
cookieStore.set(name, value, options);
}Returns:
Array<{ name: string; value: string; options: CookieOptions }> - An array of cookie deletion objects with the same structure as getCookieDataForSave(), but with:
- value: Empty string ('')
- options.maxAge: Set to 0 to expire the cookie immediately
The returned array will contain:
- Always: 1 session cookie deletion object
- Conditionally: 1 CSRF cookie deletion object (only if
enableCsrfProtectionistrue)
Wristband-Specific Methods
These methods are only relevant when using Wristband authentication for the Backend Server Integration Pattern:
fromCallback(callbackData, customFields?)
Populates session from Wristband authentication callback data.
const callbackResult = await wristbandAuth.callback(req);
session.fromCallback(callbackResult.callbackData, {
preferences: { theme: 'dark' },
lastLogin: Date.now(),
});
await session.save();Parameters:
callbackData: CallbackData- Data from Wristband auth callbackcustomFields?: Record<string, any>- Optional custom fields to merge (must be JSON-serializable)
Returns:
void
Throws:
SessionError if:
- Session has been destroyed
callbackDatais missing or invalidcustomFieldsare not JSON-serializable
getSessionResponse(metadata?)
Returns formatted session response for Wristband frontend SDKs.
app.get('/api/v1/session', async (req, res) => {
const session = await getSession(req, res, { secrets: process.env.SESSION_SECRET });
const response = session.getSessionResponse({
name: session.fullName,
preferences: session.preferences,
});
res.json(response);
});Parameters:
metadata?: Record<string, any>- Optional metadata to include
Returns:
{ tenantId: string; userId: string; metadata?: Record<string, any> }
Throws:
SessionError if session is not authenticated
getTokenResponse()
Returns formatted token response for Wristband frontend SDKs.
app.get('/api/v1/token', async (req, res) => {
const session = await getSession(req, res, { secrets: process.env.SESSION_SECRET });
const response = session.getTokenResponse();
res.json(response);
});Returns:
{ accessToken: string; expiresAt: number }
Throws:
SessionError if session is not authenticated or missing token data
Error Handling
The SDK uses a structured error system with specific error codes for different failure scenarios. All errors thrown by the SDK are instances of SessionError with a code property for programmatic error handling.
Error Types
import { SessionError, SessionErrorCode } from '@wristband/typescript-session';
try {
await session.save();
} catch (error) {
if (error instanceof SessionError) {
console.error('Error code:', error.code);
console.error('Message:', error.message);
// Optional: inspect underlying cause for debugging
if (error.cause) {
console.error('Caused by:', error.cause);
}
}
}Error Codes Reference
Session Lifecycle Errors
| Code | Description | Common Causes |
|---|---|---|
SESSION_DESTROYED |
Attempted to modify or save a destroyed session | Calling set(), save(), or other mutation methods after destroy() |
SESSION_SAVE_FAILED |
Failed to save session | Encryption failure, cookie size exceeded, serialization error |
Configuration Errors
| Code | Description | Common Causes |
|---|---|---|
INVALID_CONFIGURATION |
Invalid session configuration | Missing/invalid secrets, unsupported request type, attempting to override session methods |
MISSING_RESPONSE |
No response object available for cookie operations | Calling save() in Edge runtime (use saveToResponse() instead), missing response parameter |
Deferred Mode Errors
| Code | Description | Common Causes |
|---|---|---|
DEFERRED_MODE_NOT_ENABLED |
Attempted to flush without enabling deferred mode | Calling flush() or flushSync() without calling enableDeferredMode() first |
Wristband-Specific Errors
| Code | Description | Common Causes |
|---|---|---|
CALLBACK_DATA_INVALID |
Invalid or missing callback data | Missing callbackData or callbackData.userinfo in fromCallback() |
CUSTOM_FIELDS_NOT_SERIALIZABLE |
Custom fields cannot be serialized to JSON | Passing non-serializable values (functions, circular references) to fromCallback() |
SESSION_NOT_AUTHENTICATED |
Operation requires authenticated session | Calling getSessionResponse() or getTokenResponse() on unauthenticated session |
Common Error Scenarios
1. Invalid Configuration
try {
const session = await getSession(req, res, {
secrets: 'short' // ❌ Too short (must be 32+ chars)
});
} catch (error) {
if (error instanceof SessionError && error.code === SessionErrorCode.INVALID_CONFIGURATION) {
console.error('Configuration error:', error.message);
// "Secrets must be at least 32 characters long for security"
}
}2. Using save() in Edge Runtime
// ❌ Wrong - save() requires response object
const session = await getSession(request, { secrets: env.SESSION_SECRET });
session.set('userId', '123');
await session.save(); // Throws MISSING_RESPONSE
// ✅ Correct - use saveToResponse() in Edge runtimes
const session = await getSession(request, { secrets: env.SESSION_SECRET });
session.set('userId', '123');
const response = new Response('Success');
return session.saveToResponse(response);3. Modifying Destroyed Session
const session = await getSession(req, res, { secrets: process.env.SESSION_SECRET });
session.destroy();
try {
session.set('userId', '123'); // ❌ Throws SESSION_DESTROYED
} catch (error) {
if (error instanceof SessionError && error.code === SessionErrorCode.SESSION_DESTROYED) {
console.error('Cannot modify destroyed session');
}
}4. Invalid Callback Data
try {
session.fromCallback(null); // ❌ Missing callback data
} catch (error) {
if (error instanceof SessionError && error.code === SessionErrorCode.CALLBACK_DATA_INVALID) {
console.error('Invalid callback data:', error.message);
}
}5. Flushing Without Deferred Mode
const session = await getSession(req, res, { secrets: process.env.SESSION_SECRET });
session.set('userId', '123');
try {
await session.flush(); // ❌ Throws DEFERRED_MODE_NOT_ENABLED
} catch (error) {
if (error instanceof SessionError && error.code === SessionErrorCode.DEFERRED_MODE_NOT_ENABLED) {
console.error('Must enable deferred mode first');
}
}
// ✅ Correct usage
session.enableDeferredMode();
await session.save(); // Just marks for save
await session.flush(); // Actually savesDebugging Tips
1. Inspect Error Details and Cause Chain
try {
await session.save();
} catch (error: unknown) {
if (error instanceof SessionError) {
console.error('Session save failed:', {
code: error.code,
message: error.message,
stack: error.stack,
cause: error.cause ?? 'No underlying cause'
});
} else {
// Unexpected error type
console.error('Unexpected error during session save:', error);
}
}2. Validate Configuration Early
// Validate session config at startup
try {
const testSession = await getSession(mockRequest, {
secrets: process.env.SESSION_SECRET,
maxAge: 3600
});
console.log('Session configuration valid ✓');
} catch (error) {
if (error instanceof SessionError && error.code === SessionErrorCode.INVALID_CONFIGURATION) {
console.error('Invalid session configuration:', error.message);
process.exit(1);
}
}Advanced Topics
Deferred Mode (Node.js Performance)
ℹ️ Note on Deferred Mode
For most applications, the default behavior (encrypt on
save()) is perfectly fine. Only use deferred mode if you're modifying sessions multiple times per request or profiling shows encryption is a bottleneck. Start simple, optimize only if needed.
The problem:
By default, calling save() encrypts session data immediately. If you modify the session multiple times per request, each change triggers encryption:
session.userId = '123';
await session.save(); // Encrypts
session.lastActivity = Date.now();
await session.save(); // Encrypts againThis can become inefficient at scale for applications that modify sessions frequently.
The solution: Deferred Mode
Deferred Mode batches all saved session changes and encrypts once when you explicitly flush:
session.enableDeferredMode(); // Saves now only track if data has changed
session.userId = '123';
await session.save(); // Session knows to encrypt all changes upon flush
session.pageViews = 5;
await session.save(); // Behaves the same as the first save() call
await session.flush(); // Finally encrypts just once with all changesExpress implementation:
Express middleware runs synchronously, so you can use getSessionSync() and flushSync() to avoid async operations in the middleware chain. Hook into the response lifecycle to flush automatically before headers are sent:
import { getSessionSync } from '@wristband/typescript-session';
app.use((req, res, next) => {
req.session = getSessionSync(req, res, {
secrets: 'your-secret-key-min-32-characters-long',
maxAge: 86400,
});
req.session.enableDeferredMode();
// Auto-flush before headers are sent
const prevWriteHead = res.writeHead.bind(res);
res.writeHead = function(...args) {
if (!res.headersSent) {
req.session.flushSync(); // Synchronous flush
}
return prevWriteHead(...args);
};
next();
});Now your routes can modify the session without manual saves:
app.get('/dashboard', (req, res) => {
req.session.lastActivity = Date.now();
req.session.pageViews = (req.session.pageViews || 0) + 1;
req.session.save() // Queued
res.json({ views: req.session.pageViews });
// Encryption happens automatically before response is sent
});CSRF Protection
This SDK includes optional CSRF token protection for defense-in-depth security against cross-site request forgery attacks.
How it works:
When enabled, this SDK implements the Synchronizer Token Pattern, a widely-used CSRF defense recommended by OWASP. It works by the following:
- Token generation: CSRF tokens are generated when calling
save(),saveToResponse(), orgetCookieDataForSave() - Dual storage: Tokens are stored in two places:
- The session data (encrypted, in the
csrfTokenfield) - A separate cookie (unencrypted, so frontend JavaScript can read it; e.g.:
CSRF-TOKEN)
- Client includes token: The frontend reads the token from the cookie and includes it in request headers (e.g.,
X-CSRF-TOKEN) - Server validates: The backend validates that the header token matches the session token
- Automatic cleanup: When you destroy a session using
destroy(),destroyToResponse(), orgetCookieDataForDestroy(), both the session cookie and CSRF cookie are automatically deleted
This ensures that even if an attacker can trick a user's browser into making a request, they cannot read the token from the cookie (due to browser same-origin policy) and therefore cannot include it in the attack request.
For more details on CSRF prevention best practices, see the OWASP CSRF Prevention Cheat Sheet.
⚠️ Implementation Required
This SDK handles token generation, storage, and cookie management. However, you must implement:
- Frontend: Reading the CSRF cookie and including it in request headers
- Backend: Validating the header token matches the session token
See the "Example usage" section below for implementation patterns.
CSRF token protection is disabled by default (enableCsrfProtection: false). The SDK relies on sameSite: 'Lax' (or 'Strict') cookies for CSRF protection, which is sufficient for most applications.
When to enable:
You should enable CSRF tokens if:
- You need defense-in-depth for sensitive operations
- You're using
sameSite: 'None'(e.g., embedded iframes) - Your application requires additional CSRF guarantees beyond SameSite cookies
Basic configuration:
const session = await getSession(req, res, {
secrets: 'your-secret-key-min-32-characters-long',
maxAge: 86400,
enableCsrfProtection: true, // Enables token-based protection
});Custom configuration:
const session = await getSession(req, res, {
secrets: 'your-secret-key-min-32-characters-long',
maxAge: 86400,
enableCsrfProtection: true,
csrfCookieName: 'MY_APP_CSRF', // Default: 'CSRF-TOKEN'
csrfCookieDomain: '.example.com', // Default: same as session cookie domain
});Example usage:
The CSRF token is auto-generated when saving the session in the backend:
session.isAuthenticated = true;
await session.save(); // Generates and saves csrfTokenThe frontend reads that token from CSRF cookie:
// Get token from the cookie
const csrfToken = document.cookie
.split('; ')
.find(row => row.startsWith('CSRF-TOKEN='))
?.split('=')[1];
// Send token in request header
fetch('/api/protected', {
method: 'POST',
headers: { 'X-CSRF-TOKEN': csrfToken },
body: JSON.stringify({ data: 'bodyData' }),
});The backend validates the token value in the request header matches the csrfToken value in the session data:
app.post('/api/protected', async (req, res) => {
const session = await getSession(req, res, sessionOptions);
if (!session.isAuthenticated) {
return res.status(401).json({ error: 'Not authenticated' });
}
// Validate CSRF token
const headerToken = req.headers['X-CSRF-TOKEN'];
if (session.csrfToken !== headerToken) {
return res.status(403).json({ error: 'Invalid CSRF token' });
}
// Process request...
});CSRF cleanup on logout:
When you destroy a session using any of the destroy() functions, both the session cookie and CSRF cookie are deleted:
app.post('/logout', async (req, res) => {
const session = await getSession(req, res, sessionOptions);
session.destroy(); // Clears session data AND deletes CSRF cookie
res.json({ success: true });
});Both cookies are removed from the browser, ensuring complete cleanup of session state.
Secret Rotation
The SDK supports seamless secret rotation without invalidating existing sessions.
How it works:
const session = await getSession(req, res, {
secrets: [
'new-secret-key-min-32-characters-long', // Used for NEW sessions
'old-secret-key-min-32-characters-long', // Used to DECRYPT old sessions
],
maxAge: 86400000,
});Encryption: Always uses the first secret (secrets[0])
Decryption: Tries each secret in order until one works:
- Try
new-secret-key→ Success? Done - Try
old-secret-key→ Success? Done - All failed → Return empty session
Rotation strategy:
- Add new secret first in array:
['new', 'old'] - Deploy - Old sessions still work (decrypted with old key), new sessions use new key
- Wait for
maxAgeduration (all old sessions expire naturally) - Remove old secret:
['new']
This allows zero-downtime secret rotation. Use up to 3 secrets to support gradual rollout across multiple deployments.
Rolling Sessions
Sessions automatically roll on every save, meaning the session expiration time resets to the current time plus maxAge. This keeps active users logged in without requiring re-authentication.
How it works:
// 1) Session created at 1:00 PM, expires at 2:00 PM
const session = await getSession(req, res, {
secrets: 'your-secret-key-min-32-characters-long',
maxAge: 3600, // 1 hour
});
// 2) User makes a request at 1:30 PM...
session.lastActivity = Date.now();
await session.save();
//
// 3) Session now expires at 2:30 PM (1:30 PM + 1 hour)
//All save operations roll the session:
// All save-related functions reset expiration from current time
session.save()
session.saveToResponse()
session.getCookieDataForSave() TypeScript Support
The SDK is built with full TypeScript support and type safety. The SessionData interface includes optional Wristband fields, but you can extend it with any custom fields you want:
interface SessionData {
// Wristband fields (optional; can be used without Wristband)
isAuthenticated?: boolean;
userId?: string;
tenantId?: string;
tenantName?: string;
tenantCustomDomain?: string;
identityProviderName?: string;
accessToken?: string;
expiresAt?: number;
refreshToken?: string;
csrfToken?: string;
// Your custom fields
[key: string]: any;
}Custom Session Data Types
To add custom fields to your session for full Typescript support, do the following:
1: Create a type that extends SessionData
// src/types.ts
import { SessionData } from '@wristband/typescript-session';
interface MySessionData extends SessionData {
cartId?: string;
theme?: 'light' | 'dark';
lastVisit?: number;
}2: Initialize sessions with your type
import { getSession } from '@wristband/typescript-session';
import { MySessionData } from './types';
const session = await getSession<MySessionData>(req, options);3: Use your session with full type safety
// TypeScript knows these fields exist and their types
session.cartId = '123'; // ✅ Valid
session.theme = 'dark'; // ✅ Valid
session.theme = 'potato'; // ❌ Type error
session.lastVisit = Date.now(); // ✅ Valid
await session.save();Express: Typing
req.sessionIf you're attaching the session to Express's
Requestobject via middleware, you'll need to augment the module to typereq.session:// src/types.ts import '@wristband/typescript-session'; declare module '@wristband/typescript-session' { interface SessionData { cartId?: string; theme?: 'light' | 'dark'; lastVisit?: number; } }This makes
req.sessionfully typed throughout your Express app:app.get('/cart', (req, res) => { const cartId = req.session.cartId; // ✅ TypeScript knows this field exists // ... });
Security Considerations
Best Practices
- Secret Length: Use secrets that are at least 32 characters long to ensure 256 bits of entropy
- HTTPS Only: Always set
secure: truein production to ensure cookies are only sent over HTTPS - SameSite Cookies: Use
sameSite: 'Lax'(default) or'Strict'to prevent cross-site requests. Enable token-based CSRF protection viaenableCsrfProtection: truefor additional security. - Short Sessions: Set an appropriate
maxAgebased on your application's security requirements (e.g., 1 hour) - Secret Rotation: Rotate your secrets regularly for enhanced security (e.g., every 6 months)
Threat Model
| Attack Vector | Mitigation |
|---|---|
| Cookie theft (XSS) | ✅ HttpOnly prevents JavaScript access |
| Man-in-the-middle | ✅ Secure flag requires HTTPS |
| CSRF attacks | ✅ SameSite cookies + optional CSRF token protection |
| Session replay | ✅ Timestamp-based expiration |
| Cookie tampering | ✅ Authenticated encryption detects modifications |
| Brute force | ✅ 256-bit encryption provides strong protection |
Wristband Integration
When using with Wristband authentication, this SDK provides helper methods to simplify common auth workflows.
Auth Callback Integration
After successful Wristband authentication, use fromCallback() in your Callback Endpoint to populate session data from the callback result:
import { wristbandAuth } from './wristband-config';
import { getSession } from '@wristband/typescript-session';
app.get('/auth/callback', async (req, res) => {
// Complete Wristband auth flow
const callbackResult = await wristbandAuth.callback(req);
// Get session
const session = await getSession(req, res, {
secrets: 'your-secret-key-min-32-characters-long',
maxAge: 86400,
});
// Populate session from Wristband callback data
session.fromCallback(callbackResult.callbackData, {
// Optional: Add custom fields
preferences: { theme: 'dark' },
lastLogin: Date.now(),
});
await session.save();
res.redirect(callbackResult.callbackData.returnUrl || '/dashboard');
});What fromCallback() does:
Extracts authentication data from the callback and stores it in the session:
isAuthenticated:trueaccessToken: JWT access tokenexpiresAt: Token expiration timestampuserId: User IDtenantId: Tenant IDtenantName: Tenant nameidentityProviderName: Identity providerrefreshToken: Refresh token (ifoffline_accessscope used)tenantCustomDomain: Tenant custom domain (if applicable)
Any custom fields you pass are merged with the core session data.
Session Endpoint
The getSessionResponse() method returns session data in the format expected by Wristband frontend SDKs for your Session Endpoint:
app.get('/api/v1/session', async (req, res) => {
const session = await getSession(req, res, {
secrets: 'your-secret-key-min-32-characters-long',
maxAge: 86400,
});
// Return formatted session response with optional custom metadata
const response = session.getSessionResponse({
name: session.fullName,
email: session.email,
});
res.json(response);
});Response format:
{
"tenantId": "tenant_abc123",
"userId": "user_xyz789",
"metadata": {
"name": "John Doe",
"preferences": { "theme": "dark" }
}
}Token Endpoint
The getTokenResponse() method returns the access token and expiration time in the format expected by Wristband frontend SDKs for your Token Endpoint:
app.get('/api/v1/token', async (req, res) => {
const session = await getSession(req, res, {
secrets: 'your-secret-key-min-32-characters-long',
maxAge: 86400,
});
// Return formatted token response
const response = session.getTokenResponse();
res.json(response);
});Response format:
{
"accessToken": "eyJhbGc...",
"expiresAt": 1735689600000
}Your frontend can use this token for direct API calls to Wristband or other services:
const tokenResponse = await fetch('/api/v1/token');
const { accessToken } = await tokenResponse.json();
// Use token to call Wristband API
const userResponse = await fetch('https://your-app.wristband.dev/api/v1/users/me', {
headers: {
'Authorization': `Bearer ${accessToken}`,
},
});Using with Wristband Auth SDKs
This session SDK is embedded in the following Wristband authentication SDKs, providing both authentication and session management in a single dependency:
- @wristband/express-auth - Coming soon!
- @wristband/nextjs-auth - Coming soon!
- @wristband/nestjs-auth - Coming soon!
If you're using Wristband for authentication, we recommend using one of these framework-specific SDKs instead of installing the session SDK separately. They provide a complete, integrated solution for both authentication and session management.
Note: You only need to install this standalone session SDK if:
- You're not using Wristband authentication
- You need session management for a framework without a dedicated Wristband auth SDK
- You're building a custom integration
Questions
Reach out to the Wristband team at support@wristband.dev for any questions regarding this SDK.