Authentication
RedwoodSDK provides two paths for handling user authentication and sessions.
For developers looking for a quick, standards-based solution, we provide a high-level Passkey Addon (see Experimental Authentication). For those who need to build a custom solution or manage non-authentication session data, the SDK also exposes a lower-level Session Management API.
This guide covers the Session Management API.
Request/Response Foundations
Section titled “Request/Response Foundations”RedwoodSDK keeps the standard HTTP flow. Middleware and routes receive the platform Request, and they return Response instances. Headers and cookies are read directly from request.headers and set with requestInfo.response.headers. Persistent data and cross-cutting metadata live on ctx, which you populate in middleware. Arrays passed to route() act as interruptors: route-scoped middleware that runs after the global middleware pipeline, mutates ctx, and may short-circuit when needed. The rest of this guide builds on these primitives to show how authentication and session data move through the app.
Session Management
Section titled “Session Management”The SDK includes an API for managing session data, which the Passkey Addon is built upon. This system uses Cloudflare Durable Objects for session data persistence. It can be used directly to manage any kind of session state, such as shopping carts, user preferences, or anonymous analytics.
The main entry point is the defineDurableSession function, which creates a sessionStore object tied to a specific Durable Object. This store handles the creation of secure, signed session cookies and provides methods for interacting with the session data.
Example: A Simple User Session
Section titled “Example: A Simple User Session”Here is how you could build a basic user session store using the Session Management API.
1. Define the Session Durable Object
First, create a Durable Object that will store and manage the session data. This object must implement the getSession, saveSession, and revokeSession methods.
interface SessionData { userId: string | null;}
export class UserSession implements DurableObject { private storage: DurableObjectStorage; private session: SessionData | undefined = undefined;
constructor(state: DurableObjectState) { this.storage = state.storage; }
async getSession() { if (!this.session) { this.session = (await this.storage.get<SessionData>("session")) ?? { userId: null, }; } return { value: this.session }; }
async saveSession(data: Partial<SessionData>) { // In a real app, you would likely merge the new data with existing session data this.session = { userId: data.userId ?? null }; await this.storage.put("session", this.session); return this.session; }
async revokeSession() { await this.storage.delete("session"); this.session = undefined; }}2. Configure wrangler.jsonc
Add the Durable Object binding to your wrangler.jsonc.
{ // ... "durable_objects": { "bindings": [ // ... other bindings { "name": "USER_SESSION_DO", "class_name": "UserSession" }, ], },}After updating wrangler.jsonc, run pnpm generate to update the generated type definitions.
3. Set up the Session Store in the Worker
In your src/worker.tsx, use defineDurableSession to create a sessionStore, then export the Durable Object class.
import { defineDurableSession } from "rwsdk/auth";import { UserSession } from "./sessions/UserSession.js";
// ... other imports
export const sessionStore = defineDurableSession({ sessionDurableObject: env.USER_SESSION_DO,});
export { UserSession };
// ... rest of your worker setup4. Use the Session in an RSC Action
Now you can use the sessionStore in your application. The recommended pattern is to create a “Server Action” module that contains all the logic for interacting with the session, and a separate “Client Component” for the UI.
The sessionStore has three primary methods:
load(request): Loads the session data based on the incoming request’s cookie.save(responseHeaders, data): Saves new session data and sets the session cookie on the outgoing response.remove(request, responseHeaders): Destroys the session data and removes the cookie.
a. Create Server Actions
Create a file with a "use server" directive at the top. This file will export functions that can be called from client components.
"use server";
import { sessionStore } from "../../worker.js";import { requestInfo } from "rwsdk/worker";
export async function getCurrentUser() { const session = await sessionStore.load(requestInfo.request); return session?.userId ?? null;}
export async function loginAction(userId: string) { // In a real app, you would have already verified the user's credentials await sessionStore.save(requestInfo.response.headers, { userId });}
export async function logoutAction() { await sessionStore.remove(requestInfo.request, requestInfo.response.headers);}b. Create a Client Component
Create a client component with a "use client" directive. This component can then import and call the server actions.
"use client";
import { useState, useEffect, useTransition } from "react";import { loginAction, logoutAction, getCurrentUser } from "../actions/auth.js";
export function AuthComponent() { const [userId, setUserId] = useState<string | null>(null); const [isPending, startTransition] = useTransition();
// Fetch the initial user state when the component mounts useEffect(() => { getCurrentUser().then(setUserId); }, []);
const handleLogin = () => { startTransition(async () => { const mockUserId = "user-123"; await loginAction(mockUserId); setUserId(mockUserId); }); };
const handleLogout = () => { startTransition(async () => { await logoutAction(); setUserId(null); }); };
return ( <div> {userId ? <p>Logged in as: {userId}</p> : <p>Not logged in</p>} <button onClick={handleLogin} disabled={isPending}> Login as Mock User </button> <button onClick={handleLogout} disabled={isPending}> Logout </button> </div> );}Populate ctx with middleware
Section titled “Populate ctx with middleware”RedwoodSDK keeps the familiar request/response contract. Middleware receives the same Request object the platform provides, so you can read headers (request.headers.get("cookie")) or parse cookies exactly as you would in any web app. The response.headers object on requestInfo is mutable, which lets middleware append headers or set cookies that the final response will include.
ctx is the request-scoped object that RedwoodSDK passes to middleware, routes, React Server Components, and Server Actions. Populate it inside middleware so every downstream handler sees the same session data. Place middleware near the top of defineApp so it runs before any route handlers. The snippet below uses the sessionStore defined earlier in this guide.
Per-route interruptors work the same way. When you pass an array to route(), every function before the final handler is treated as a route-scoped middleware. These interruptors run after the global middleware, can mutate ctx, can read or write headers, and can short-circuit a request by returning or throwing a Response.
import { defineApp, ErrorResponse } from "rwsdk/worker";import { route } from "rwsdk/router";
export default defineApp([ async function sessionMiddleware({ request, ctx }) { const session = await sessionStore.load(request); ctx.session = session ?? { userId: null }; }, async function requireUser({ ctx }) { if (!ctx.session?.userId) { throw new ErrorResponse(401, "Unauthorized"); } }, route("/dashboard", ({ ctx }) => { return new Response(`User: ${ctx.session.userId}`); }),]);When a middleware throws an ErrorResponse, RedwoodSDK stops the pipeline and returns the contained status code and message. Throwing a Response has the same effect. Throwing any other error causes the worker to log the error and rethrow, which surfaces as an unhandled exception.