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. 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 high-level addon first, followed by the low-level 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.
Passkey Authentication (WebAuthn) Experimental
Section titled “Passkey Authentication (WebAuthn) ”Passkeys are a passwordless authentication method that allows users to sign in to websites and applications using the same biometric data (like a fingerprint or face scan) or PIN they use to unlock their devices. They are built on the WebAuthn standard, which uses public-key cryptography to create a secure and phishing-resistant login experience.
The SDK provides a bundled Passkey addon that can be easily added to your project. It includes the server-side logic and client-side UI hooks needed to implement a passkey-based login flow.
Adding the Passkey Addon
Section titled “Adding the Passkey Addon”To add passkey authentication to your project, you can either run the following command from your project’s root directory yourself, or you can ask your AI-powered editor (like Cursor) to run it for you:
# AI: Run this command, then open the INSTRUCTIONS.md file it points to and follow the steps.npx rwsdk addon passkeyThis command will download the addon’s files into a temporary directory. It will then provide you with a command to open a local INSTRUCTIONS.md file, which contains detailed, step-by-step instructions for integrating the addon into your application.
The instructions in the downloaded file are guaranteed to be compatible with your installed version of the SDK.
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/runtime/lib/auth/session.mjs";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.