# Authentication (https://docs.rwsdk.com/core/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](/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 [#requestresponse-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 [#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 [#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. ```typescript title="src/sessions/UserSession.ts" 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("session")) ?? { userId: null, }; } return { value: this.session }; } async saveSession(data: Partial) { // 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`. ```jsonc title="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. ```typescript title="src/worker.tsx" 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 setup ``` **4. 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. ```typescript title="src/app/actions/auth.ts" "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. ```tsx title="src/app/components/AuthComponent.tsx" "use client"; import { useState, useEffect, useTransition } from "react"; import { loginAction, logoutAction, getCurrentUser } from "../actions/auth.js"; export function AuthComponent() { const [userId, setUserId] = useState(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 (
{userId ?

Logged in as: {userId}

:

Not logged in

}
); } ``` ### Populate `ctx` with middleware [#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`. ```tsx title="src/worker.tsx" 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. --- # Cron Triggers (https://docs.rwsdk.com/core/cron) If you want to schedule a background task, Cloudflare supports [Cron Triggers](https://developers.cloudflare.com/workers/configuration/cron-triggers/). ℹ️ **Important:** Cron triggers only fire automatically after you deploy to Cloudflare. The local dev server does not schedule jobs for you, but you can still trigger the scheduled cron handler manually (see [Testing locally](#testing-locally) below). ## Setup [#setup] Within your `wrangler.jsonc` file, add a new section called `triggers`: ```jsonc title="wrangler.jsonc" "triggers": { "crons": ["* * * * *"] } --- Where `crons` includes an array of cron schedules. --- ``` After updating `wrangler.jsonc`, run `pnpm generate` to update the generated type definitions. Within your `worker.tsx` file, adjust your `defineApp` function: ```tsx const app = defineApp([ ... ]); export default { fetch: app.fetch, async scheduled(controller: ScheduledController) { switch (controller.cron) { case "* * * * *": { console.log("🧹 Run minute-by-minute cleanups"); break; } case "0 * * * *": { console.log("📈 Aggregate hourly metrics"); break; } case "0 21 * * *": { console.log("🌙 Kick off nightly billing at 9 PM UTC"); break; } default: { console.warn(`Unhandled cron: ${controller.cron}`); } } console.log("⏰ cron processed"); }, } satisfies ExportedHandler; ``` Notice each `case` matches a cron schedule that must exist in your `wrangler.jsonc` file: ```jsonc title="wrangler.jsonc" "crons": ["* * * * *", "0 * * * *", "0 21 * * *"] ``` ## Testing locally [#testing-locally] To simulate a scheduled run in development, hit the dev server's scheduler endpoint and pass the cron expression you want to test: ```bash curl "http://localhost:5173/cdn-cgi/handler/scheduled?cron=*+*+*+*+*" ``` For example, run the following commands to see the above cron handlers in action: * Every minute: ```bash curl "http://localhost:5173/cdn-cgi/handler/scheduled?cron=*+*+*+*+*" ⏰ cron processed ``` * Every hour on the zero minute: ```bash curl "http://localhost:5173/cdn-cgi/handler/scheduled?cron=0+*+*+*+*" 📈 Aggregate hourly metrics ⏰ cron processed ``` * Every day at 9 PM UTC: ```bash curl "http://localhost:5173/cdn-cgi/handler/scheduled?cron=0+21+*+*+*" 🌙 Kick off nightly billing at 9 PM UTC ⏰ cron processed ``` ## Cloudflare Cron Triggers [#cloudflare-cron-triggers] Within the Cloudflare Dashboard UI, you can view all the cron triggers for your worker. Click on the **Settings** tab, and then click on the **Cron Triggers** section. To view more details about the events, click on the **View Events** link. You can see a list of all the events that have been triggered for your worker. ## Further Reading [#further-reading] * [Cloudflare Cron Triggers](https://developers.cloudflare.com/workers/configuration/cron-triggers/) --- # Email (https://docs.rwsdk.com/core/email) RedwoodSDK integrates with [Cloudflare Email Workers](https://developers.cloudflare.com/email-routing/email-workers/) so your application can send transactional messages, receive inbound mail, and reply in the same Worker runtime. Production deliveries currently require recipients to be verified through Cloudflare Email Routing, but Cloudflare’s forthcoming [Email Service beta](https://blog.cloudflare.com/email-service/) expands reach to general addresses. This guide walks through the configuration steps, highlights important sending considerations, and demonstrates common patterns for end-to-end email workflows. ## Implementing Email Handling [#implementing-email-handling] Update your `wrangler.jsonc` to include the `EMAIL` binding: ```jsonc title="wrangler.jsonc" { "send_email": [ { "name": "EMAIL", }, ], } ``` Next, run `pnpm generate` to update the generated type definitions. Once you have a zone with Email Routing enabled, follow the [Enable Email Workers](https://developers.cloudflare.com/email-routing/email-workers/enable-email-workers/) documentation to deploy your Worker in production. Outbound email **must target a destination address that you have verified** in [Email Routing](https://developers.cloudflare.com/email-routing/email-workers/enable-email-workers/). When calling `env.EMAIL.send()`, pass either a verified address or leave the recipient undefined when you use a binding that specifies `destination_address` or `allowed_destination_addresses`. > ℹ️ For broader transactional delivery to arbitrary recipients, see [Cloudflare's Email Service beta](https://blog.cloudflare.com/email-service/) or an external provider such as [Resend](/guides/email/sending-email). ### Example Worker with Email Handling [#example-worker-with-email-handling] The example below demonstrates how to integrate the worker with email sending and receiving. To send and email, you can simply call the `env.EMAIL.send()` method with the email message as shown in the example below `route("/email", async () => { ... })`. > ℹ️ Note: In production, this must be a verified address in [Email Routing](https://developers.cloudflare.com/email-routing/email-workers/enable-email-workers/). However, to receive an email, you need to implement the `email` handler and export it in the default export of the worker. This is a change to how `defineApp` is often used in the worker. The default export of the worker is the `DefaultWorker` class that extends the `WorkerEntrypoint` class. This tells Cloudflare that the worker is also email worker and can be selected to route inbound emails to. > Note: If you are just sending emails, you can still use `defineApp` as usual and just call `env.EMAIL.send()` in the route handler or in a server function. ```ts title="worker.ts" import * as PostalMime from "postal-mime"; import { EmailMessage } from "cloudflare:email"; import { createMimeMessage } from "mimetext"; import { render, route } from "rwsdk/router"; import { defineApp } from "rwsdk/worker"; import { Document } from "@/app/Document"; import { setCommonHeaders } from "@/app/headers"; import { env, WorkerEntrypoint } from "cloudflare:workers"; const app = defineApp([ setCommonHeaders(), /** * This route is used to send an email from the worker * First, we create a MIME message with the sender, recipient, * and the content of the email. * Then, we create a new EmailMessage object with the * sender, recipient, and the raw content of the email. * Finally, we send the email using the `env.EMAIL.send()` method. * Ensure the `recipient@example.com` address is verified in Cloudflare Email Routing, or adjust the binding configuration accordingly. */ route("/email", async () => { const msg = createMimeMessage(); msg.setSender({ name: "Sending email test", addr: "sender@example.com" }); msg.setRecipient("recipient@example.com"); msg.setSubject("An email generated in a worker"); msg.addMessage({ contentType: "text/plain", data: `Congratulations, you just sent an email from a worker.`, }); const message = new EmailMessage( "sender@example.com", "recipient@example.com", msg.asRaw() ); await env.EMAIL.send(message); return Response.json({ ok: true }); }), ]); /** * This is the default worker entrypoint for the Worker. * It extends the WorkerEntrypoint class and implements the email and fetch handlers. */ // It extends the WorkerEntrypoint class and implements the email and fetch handlers. export default class DefaultWorker extends WorkerEntrypoint { /** * Email handler for the Worker. * The `message` parameter is an ForwardableEmailMessage object * * You can call `message.reply()` to respond directly to the * inbound sender without additional verification steps. */ async email(message: ForwardableEmailMessage) { const parser = new PostalMime.default(); const rawEmail = new Response((message as any).raw); const email = await parser.parse(await rawEmail.arrayBuffer()); console.log(email); } /** * Fetch handler for the Worker. * Needed so that the worker can handle the request and pass it to the app. */ override async fetch(request: Request) { return await app.fetch(request, this.env, this.ctx); } } ``` ### Replying to inbound email [#replying-to-inbound-email] You can reply directly to an inbound message without pre-verifying the recipient. The Worker runtime preserves threading headers and delivers the response through the original route. Construct your response with `mimetext` and pass the raw payload to `message.reply()`, as shown in the [Reply from Workers guide](https://developers.cloudflare.com/email-routing/email-workers/reply-email-workers/). It is important to note that the `In-Reply-To` header is required to reply to the inbound email. ```ts title="Replying inside the email handler" async email(message: ForwardableEmailMessage) { console.log("📧 Email received"); // Parse the inbound email const parser = new PostalMime.default(); const rawEmail = new Response((message as any).raw); const receivedEmail = await parser.parse(await rawEmail.arrayBuffer()); console.log("📧 Email received and parsed", receivedEmail); // Create a new message to reply to the inbound email const replyToMessage = createMimeMessage(); // ❗️ Important:This In-Reply-To header is required to reply to the inbound email replyToMessage.setHeader( "In-Reply-To", message.headers.get("Message-ID") ?? "" ); replyToMessage.setSender({ name: "Contact Person", addr: "@example.com" }); replyToMessage.setRecipient(receivedEmail.from); replyToMessage.setSubject(`Re: ${receivedEmail.subject}`); replyToMessage.addMessage({ contentType: "text/plain", data: "Thanks for contacting us. We'll get back to you shortly.", }); console.log("📧 New message created", replyToMessage.asRaw()); const replyMessage = new EmailMessage( "@example.com", message.from, replyToMessage.asRaw() ); console.log("📧 Sending reply email"); await message.reply(replyMessage); console.log("📧 Reply email sent"); } ``` ### Key points [#key-points] * By default, the worker is not an email worker. You need to extend the `WorkerEntrypoint` class and implement the `email` handler to make it an email worker. * By extending the `WorkerEntrypoint` class, you are telling Cloudflare that the worker is also email worker and can be selected to route inbound emails to. * The `message` parameter is an `ForwardableEmailMessage` object that contains the inbound email message. * The `In-Reply-To` header is required to reply to the inbound email. * Use `PostalMime` to parse inbound messages for headers, text, HTML, and attachments. * Construct outbound MIME content with `mimetext` to control subject, sender, and body. * Call `env.EMAIL.send()` to deliver new messages, or `message.reply()` / `message.forward()` inside the email handler. * A fetch handler is needed so that the worker can handle all requests and pass it to the app. ## Testing Locally [#testing-locally] RedwoodSDK can [emulate](https://developers.cloudflare.com/email-routing/email-workers/local-development/) both inbound and outbound email interactions locally. ### Sending Email Locally [#sending-email-locally] To test the sending of an email by the email handler locally, you can use the following command: ```bash pnpm dev ``` This will start the local development server and you can send emails to the `recipient@example.com` address. Now, visit `http://localhost:5173/email` to see the email in the console output of the local development server. For this example, you'll see the following response: ```json title="Response" { "ok": true } ``` and in the console output, you'll see something like the following log: ```bash send_email binding called with the following message: /var/folders/ft/8320mthj6gbdd2pmc42x13480000gn/T/miniflare-288e7109e15f898bd9877d7857386f8b/files/email/2dad29db-0a7d-498d-89ab-e961746835c4.eml ``` You can also see the email in the `.eml` file in the temporary directory. ```bash cat /var/folders/ft/8320mthj6gbdd2pmc42x13480000gn/T/miniflare-288e7109e15f898bd9877d7857386f8b/files/email/2dad29db-0a7d-498d-89ab-e961746835c4.eml 288e7109e15f898bd9877d7857386f8b/files/email/2dad29db-0a7d-498d-89ab-e961746835c4.eml Date: Sun, 09 Nov 2025 01:54:06 +0000 From: =?utf-8?B?U2VuZGluZyBlbWFpbCB0ZXN0?= To: Message-ID: Subject: =?utf-8?B?QW4gZW1haWwgZ2VuZXJhdGVkIGluIGEgd29ya2Vy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 7bit Congratulations, you just sent an email from a worker.% ``` > Note: The path to the `.eml` file is different for each operating system. ### Receiving Email Locally [#receiving-email-locally] To test the receiving of an email by the email handler locally, you can use the following command: ```bash pnpm dev ``` This will start the local development server and you can send emails to the `recipient@example.com` address. Then, you can send an email to the `recipient@example.com` address using the following command: ```bash curl --request POST 'http://localhost:5173/cdn-cgi/handler/email' \ --url-query 'from=sender@example.com' \ --url-query 'to=recipient@example.com' \ --header 'Content-Type: application/json' \ --data-raw 'Received: from smtp.example.com (127.0.0.1) by cloudflare-email.com (unknown) id 4fwwffRXOpyR for ; Tue, 27 Aug 2024 15:50:20 +0000 From: "John" Reply-To: sender@example.com To: recipient@example.com Subject: Testing Email Workers Local Dev Content-Type: text/html; charset="windows-1252" X-Mailer: Curl Date: Tue, 27 Aug 2024 08:49:44 -0700 Message-ID: <6114391943504294873000@ZSH-GHOSTTY> Hi there' ``` You should see the content of the simulated email in the console output of the local development server. ```bash { headers: [ { key: 'received', value: 'from smtp.example.com (127.0.0.1) by cloudflare-email.com (unknown) id 4fwwffRXOpyR for ; Tue, 27 Aug 2024 15:50:20 +0000' }, { key: 'from', value: '"John" ' }, { key: 'reply-to', value: 'sender@example.com' }, { key: 'to', value: 'recipient@example.com' }, { key: 'subject', value: 'Testing Email Workers Local Dev' }, { key: 'content-type', value: 'text/html; charset="windows-1252"' }, { key: 'x-mailer', value: 'Curl' }, { key: 'date', value: 'Tue, 27 Aug 2024 08:49:44 -0700' }, { key: 'message-id', value: '<6114391943504294873000@ZSH-GHOSTTY>' } ], from: { address: 'sender@example.com', name: 'John' }, to: [ { address: 'recipient@example.com', name: '' } ], replyTo: [ { address: 'sender@example.com', name: '' } ], subject: 'Testing Email Workers Local Dev', messageId: '<6114391943504294873000@ZSH-GHOSTTY>', date: '2024-08-27T15:49:44.000Z', html: 'Hi there\n', attachments: [] } ``` ## Production Deployment [#production-deployment] To enable email handling in production, you need to have a Cloudflare zone with Email Routing enabled and at least one verified destination address. You can refer to the following documentation: * [Configure Email Routing Rules and Addresses](https://developers.cloudflare.com/email-routing/setup/email-routing-addresses/) * [Enable Email Workers](https://developers.cloudflare.com/email-routing/email-workers/enable-email-workers/) * [Send Email from Workers](https://developers.cloudflare.com/email-routing/email-workers/send-email-workers/) * [Reply to Email from Workers](https://developers.cloudflare.com/email-routing/email-workers/reply-email-workers/) * [Cloudflare Email Service Beta](https://blog.cloudflare.com/email-service/) * [Sending email with Resend](/guides/email/sending-email) ## Further Reading [#further-reading] * [Cloudflare Email Routing Documentation](https://developers.cloudflare.com/email-routing/email-workers/) * [Local Development for Email Workers](https://developers.cloudflare.com/email-routing/email-workers/local-development/) ## Future Improvements [#future-improvements] * Demonstrate how to compose emails with [React Email](https://react.email). --- # Environment Variables & Secrets (https://docs.rwsdk.com/core/env-vars) When integrating with external services you typically store your credentials in environment variables. This is done to avoid hardcoding secrets into your codebase. There are several environments that require different credentials, for example: * On your local machine: Development * Secrets on deployed Workers: Staging & Production ## Development [#development] Create a `.env` file in the root of your project. ```ts title=".env" SECRET_KEY = "value"; API_TOKEN = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; ``` Cloudflare uses `.dev.vars`, however, `.env` is the typical approach. Therefore, when you run `pnpm dev`, RedwoodSDK will automatically create a symlink from `.env` to `.dev.vars`. ### Updating Types [#updating-types] After adding any environment variables, run: ```bash npx wrangler types ``` This adds the environment variable and associated type to `worker-configuration.d.ts` and avoids unknown types when accessing `env`. ```ts title="worker-configuration.d.ts" // Generated by Wrangler by running `wrangler types` // Runtime types generated with .... declare namespace Cloudflare { interface Env { SECRET_KEY: string; API_TOKEN: string; } } ``` ## Production / Secrets on deployed Workers [#production--secrets-on-deployed-workers] To add a secret to a deployed worker, run: ```bash npx wrangler secret put ``` Then, the CLI will prompt you to enter the secret value. These can also be added and managed via the Cloudflare dashboard.
  1. Expand the Computer (Workers) tab
  2. Click on Workers and Pages
  3. Click on the name of the worker
  4. Click on the "Settings" tab
  5. Click on the "Variables and Secrets" section
## Using an Environment Variable [#using-an-environment-variable] At the top of your file, import `env`: ```tsx import { env } from "cloudflare:workers"; ``` Then, you can access the environment variables through the `env` object. ```ts const rpID = env.WEBAUTHN_RP_ID ?? new URL(request.url).hostname; ``` ## Managing staging and production configurations [#managing-staging-and-production-configurations] Define each Cloudflare environment in `wrangler.jsonc`. Wrangler reads the `env` block to decide which variables, routes, and bindings to apply when you deploy with `CLOUDFLARE_ENV`. ```jsonc title="wrangler.jsonc" { "name": "redwood-example", "main": "./dist/worker.mjs", "compatibility_date": "2024-10-21", "env": { "staging": { "vars": { "APP_BASE_URL": "https://staging.example.com" }, "routes": [ { "pattern": "staging.example.com/*", "custom_domain": true } ] } } } ``` After updating `wrangler.jsonc`, run `pnpm generate` to update the generated type definitions. Create environment-scoped secrets with the `--env` flag: ```bash npx wrangler secret put DATABASE_URL --env staging ``` Deploy with `CLOUDFLARE_ENV=staging` to load the staging configuration, or omit it to deploy the default production configuration. ## Further Reading [#further-reading] * [Cloudflare Environment Variables](https://developers.cloudflare.com/workers/configuration/secrets/) --- # Hosting (https://docs.rwsdk.com/core/hosting) Cloudflare's Developer Platform provides out-of-the-box access to essential services: * [Compute](https://developers.cloudflare.com/workers/) (Workers) for serverless functions * [Database](https://developers.cloudflare.com/d1/) (D1) for data storage * [Storage](https://developers.cloudflare.com/r2/) (R2) for files and assets * [Queues](https://developers.cloudflare.com/queues/) for background job processing * & [so much more!](https://developers.cloudflare.com/) Not only does Cloudflare have the world's best network, but they also have the best developer experience. When you code locally you're coding against a real environment that is the same as the production environment. This has huge implications for your workflow and productivity, because often "it just works!" ## Deploy to production [#deploy-to-production] Ship your webapp to Cloudflare with the following command: Within the Terminal, it will ask you: **Do you want to proceed with deployment? (y/N):** Type `y` and press Enter. Go to your dashboard in Cloudflare, on the left side navigation click on **Workers & Pages**. You should see your application in the list. Then, you can click on the **Visit** link to see your application online. ## Deploy to staging [#deploy-to-staging] Staging deployments reuse the same release process but load configuration from the matching entry under `env` in `wrangler.jsonc`. Set the `CLOUDFLARE_ENV` variable before running the release command: ```bash CLOUDFLARE_ENV=staging pnpm release ``` Wrangler applies the `env.staging` routes, bindings, and variables defined in your configuration file. The terminal output includes the staging Worker URL. Cloudflare also lists the latest staging deployment under Workers & Pages so you can confirm the release completed. ## Using a Custom Domain Name [#using-a-custom-domain-name] You can use a custom domain name with your application. You can purchase a domain name through Cloudflare, or you can use an existing domain name you already own. **If you already have a domain name that's active on your Cloudflare account, you can skip right to [Hooking Up your Domain Name to your Project](#hooking-up-your-domain-name-to-your-project). Otherwise, read on!** ### Adding a Domain Name to Cloudflare [#adding-a-domain-name-to-cloudflare] To add a domain name to Cloudflare, you have two options: 1. **Purchase a new domain name** through Cloudflare. 2. **Add an existing domain name** to Cloudflare. #### Purchase a new domain name [#purchase-a-new-domain-name] If you don't already have a domain name, it's super easy to just buy one through Cloudflare. This is the easiest option, and will let you host your site on your new domain name right away. To buy a domain name through Cloudflare, go to [Cloudflare's Domain Registrar](https://domains.cloudflare.com/) and search for the domain name you want to buy. If it's available, you can purchase it right here, and it will be automatically added to your Cloudflare account. #### Add an existing domain name [#add-an-existing-domain-name] If you already have a domain name, or simply prefer to use a different registrar, you can add your domain to Cloudflare as follows: Head to your Cloudflare dashboard, and click on the **+ Add a domain** button: Next, search for the domain name you want to add. You can keep the "Quick scan for DNS records" option checked. Click **Continue**. Cloudflare will ask you to select a plan for this domain. You can select the **Free** plan, which is perfect for most use cases (and you can always upgrade later, if you need). Click **Select plan**. Next, you'll be asked to review the DNS records that Cloudflare found for your domain. If you have any existing DNS records, they will be automatically imported here. You can add or remove any records as needed, and you can always come back to this later. Click **Continue to activation**. Now's the part where you tell your domain registar to use Cloudflare's nameservers. This is how Cloudflare will be able to manage your domain name. To do this, you'll need to know how to change your nameservers — every registrary is different, so you'll need to look up the instructions for your specific registrar. Here are some common registrars and their instructions: * [Porkbun](https://kb.porkbun.com/article/22-how-to-change-your-nameservers) * [Namecheap](https://www.namecheap.com/support/knowledgebase/article.aspx/767/10/how-to-change-dns-for-a-domain/) * [GoDaddy](https://www.godaddy.com/help/edit-my-domain-nameservers-664) Cloudflare also has a long list of links to instructions for many registrars [here](https://developers.cloudflare.com/dns/nameservers/update-nameservers/#your-domain-uses-a-different-registrar). Once you've updated your nameservers, go back to Cloudflare and click **Continue**. It'll now present you with a note that it can take some time to process nameserver changes. From here, you can just wait until Cloudflare emails you, but for the impatiant amongst us, Cloudflare offers a **Check nameservers now** button. Clicking this will trigger a check to see if your nameservers have been updated. Once Cloudflare has confirmed that your nameservers have been updated, you'll get an email, and your domain will be added to your Cloudflare account. ### Hooking Up your Domain Name to your Project [#hooking-up-your-domain-name-to-your-project] To use a domain name that's registered to your Cloudflare account, go to **Workers & Pages** in the left side navigation. Then, click on the name of your project. Click on the **Settings** tab. At the top, you'll see the domains associated with your project. Click on the **+ Add** button at the top of the **Domains & Routes** table. A side panel will appear: Click on the **Custom Domain** option. Then, it will ask you enter the domain name you want to use. And that's it! Your domain name is now connected to your project. You can now visit your project at your custom domain name. ## Deleting Your Project [#deleting-your-project] If for whatever reason, you need to delete your project through Cloudflare, go to **Workers & Pages** in the left side navigation. Then, click on the name of your project. Click on the **Settings** tab, then scroll to the bottom of the page. Click on the **Delete** button. A confirmation modal will appear, asking you to type the name of your project, then click on the **Delete** button. --- # Overview (https://docs.rwsdk.com/core/overview) Below are the things you need to know in order to be effective with RedwoodSDK. *Peter Pistorius gives a 5 minute tour of RedwoodSDK.* *** 1. [Request Handling, Routing & Responses](/core/routing) 2. [React Server Components](/core/react-server-components) 3. [Database](/experimental/database) 4. [Storage](/core/storage) 5. [Realtime](/experimental/realtime) 6. [Queues & Background Jobs](/core/queues) 7. [Email](/core/email) 8. [Authentication & Session Management](/core/authentication) 9. [Security](/core/security) 10. [Hosting (Cloudflare)](/core/hosting) --- # Queues (https://docs.rwsdk.com/core/queues) Whilst building a webapp you want to be able to respond as quickly as possible to user interactions, but sometimes you need to do things that take a long time! For example, you might need to send an email when a user submits a form, or you need to process a payment, or do something magic with AI - and you don't want the user to wait for these things to complete. To handle these you'll use **background tasks**. Background tasks are managed by the [Cloudflare queue system](https://developers.cloudflare.com/queues/). You send a message to a queue, where a worker will process the message, but on a different worker - so it doesn't block the main one. ### Setup [#setup] First thing you've got to do is create a queue and bind the queue producers and consumers to your worker. ```bash npx wrangler queues create my-queue-name ``` Replace `my-queue-name` with the name of your queue, and place the following in your `wrangler.jsonc` file: ```jsonc title="wrangler.jsonc" { "queues": { "producers": [ { "binding": "QUEUE", "queue": "my-queue-name", } ], "consumers": [ { "queue": "my-queue-name", "max_batch_size": 10, "max_batch_timeout": 5 } ] } } ``` After updating `wrangler.jsonc`, run `pnpm generate` to update the generated type definitions. This will bind the queue to the `env.QUEUE` object in the worker. So you'll be able to send messages. #### Naming Queues [#naming-queues] Queue names must match the following RegEx pattern: `^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$` ##### Valid queue names [#valid-queue-names] * `my-queue` * `my-awesome-queue-123` * `queue1` * `1queue` * `my-queue-v2` ##### Invalid queue names [#invalid-queue-names] * `My_Queue` (uppercase letters not allowed) * `MY_QUENE_NAME` (uppercase letters and underscores not allowed)) * `-queue-` (cannot start or end with hyphen) * `really-really-really-really-really-really-really-long-queue-name` (max 63 chars) * `queue_name` (underscores not allowed) ### Sending messages [#sending-messages] ```tsx title="src/worker.tsx" import { env } from "cloudflare:workers"; export default defineApp([ route('/pay-with-ai', () => { // Post a message to the queue env.QUEUE.send({ userId: 1, amount: 100, currency: 'USD', }) return new Response('Done!') }) ]) ``` ### Receiving messages [#receiving-messages] In order to "consume messages" from the queue you need to change the shape of the `default` export of your worker. You'll add another function called `queue` that will receive a batch of messages. ```tsx title="src/worker.tsx" const app = defineApp([ /* routes... */]) export default { fetch: app.fetch, async queue(batch) { // [!code highlight:5] for (const message of batch.messages) { console.log('handling message' + JSON.stringify(message)) } } } satisfies ExportedHandler; ``` This will receive a batch of messages, and process them one by one. ## Ways to Send Messages [#ways-to-send-messages] Cloudflare Queues allow Workers to send and process asynchronous messages reliably. There are three common approaches to send data: ### Send Message Body Directly (up to 128KB) [#send-message-body-directly-up-to-128kb] Best for: Small payloads that fit within the 128KB limit. ```ts await queue.send({ body: JSON.stringify({ email: "user@example.com", subject: "Welcome!" }), }); ``` ✅ Simple and fast ❌ Hard limit of 128KB per message ### Store in R2 and Send Object Key [#store-in-r2-and-send-object-key] Best for: Large payloads (e.g., files, JSON blobs, videos). ```ts // Upload to R2 first await r2.put("msg/123.json", JSON.stringify(largeData)); // Then send only the key to the queue await queue.send({ body: JSON.stringify({ r2Key: "msg/123.json" }), }); ``` ✅ Great for large data ✅ Persistent and versioned if needed ❌ Slightly more complex (requires R2 integration) ### Store in KV and Send KV Key [#store-in-kv-and-send-kv-key] Best for: Short-lived messages or small-to-medium payloads. ``` // Save to KV await kv.put("queue:msg:123", JSON.stringify(data), { expirationTtl: 600 }); // Send reference key await queue.send({ body: JSON.stringify({ kvKey: "queue:msg:123" }), }); ``` ✅ Fast access ✅ Automatic expiration possible ❌ Not ideal for large data ❌ KV has eventual consistency (meaning that when you write data to Cloudflare KV (via kv.put), it might not be immediately visible to all readers — especially in different Cloudflare data centers.) ### Tips [#tips] #### Handling Different Queues [#handling-different-queues] By sending a message on a queue ```tsx title="src/worker.tsx" import { env } from "cloudflare:workers"; export default defineApp([ route('/pay-with-ai', () => { // Post a message to the queue env.QUEUE.send({ userId: 1, amount: 100, currency: 'USD', }) return new Response('Done!') }) ]) ``` when handling queues and receivng a `MessageBatch`, the `batch` contains a collection of `messages` and the name of the `queue` which you can use to handle ```tsx title="src/worker.tsx" const app = defineApp([ /* routes... */]) export default { fetch: app.fetch, async queue(batch) { // [!code highlight:5] if (batch.queue === 'my-queue-name') { for (const message of batch.messages) { console.log('handling my-queue-name message' + JSON.stringify(message)) } } } } satisfies ExportedHandler; ``` > ℹ️ Note: Having a dedicated Queue for a specific message is a best practice #### Handling Different Messages on the Same Queue [#handling-different-messages-on-the-same-queue] Use metadata (e.g. type, source, key) in your message body to help the consumer Worker determine where and how to retrieve the full data. If for some reason, you decide to share a queue for different types of messages, one pattern is to set a `type` (or other attribute) in the message body to specify its purpose and how to handle its contents. For example, when sending this payment message: ```tsx title="src/worker.tsx" import { env } from "cloudflare:workers"; export default defineApp([ route('/pay-with-ai', () => { // Post a message to the queue env.QUEUE.send({ type: 'PAYMENT', userId: 1, amount: 100, currency: 'USD', }) return new Response('Done!') }) ]) ``` One can then determine the message type and handle accordingly: ```tsx title="src/worker.tsx" const app = defineApp([ /* routes... */]) export default { fetch: app.fetch, async queue(batch) { // [!code highlight:5] for (const message of batch.messages) { const { type, userId, amount, currenct } = message.body as { type: string, userId: number, amount: number, currency: string }; if (type === 'PAYMENT') { console.log('handling payment message' + JSON.stringify(message)) } } } } satisfies ExportedHandler; ``` --- # React Server Components (https://docs.rwsdk.com/core/react-server-components) React is used to build your user interface. By default, all components are server components. That means that the component is rendered on the server as HTML and then streamed to the client. These do not include any client-side interactivity. ```tsx export default function MyServerComponent() { return
Hello, from the server!
; } ``` When a user needs to interact with your component: clicking a button, setting state, etc, then you must use a client component. Mark the client component with the `"use client"` directive. This will be hydrated by React in the browser. ```tsx "use client"; // [!code highlight] export default function MyClientComponent() { return ; } ``` ## Fetching and displaying data [#fetching-and-displaying-data] React Server Components run on the server, they can easily fetch data and make it part of the payload that's sent to the client. ```tsx title="src/app/pages/todos/TodoPage.tsx" // [!code word:async] export async function Todos({ ctx }) { const todos = await db.todo.findMany({ where: { userId: ctx.user.id } }); return (
    {todos.map((todo) => (
  1. {todo.title}
  2. ))}
); } export async function TodoPage({ ctx }) { return (

Todos

Loading...
}> ); } ``` The `TodoPage` component is a server component. It is rendered by a route, so it receives the `ctx` object. We pass this to the `Todos` component, which is also a server component, and renders the todos. ## Server Functions [#server-functions] Allow you to execute code on the server from a client component. ```tsx title="@/pages/todos/functions.tsx" "use server"; // [!code highlight] import { requestInfo } from "rwsdk/worker"; export async function addTodo(formData: FormData) { const { ctx } = requestInfo; const title = formData.get("title"); await db.todo.create({ data: { title, userId: ctx.user.id } }); } ``` The `addTodo` function is a server function. It is executed on the server when the form is submitted from a client side component. The form data is sent to the server and the function is executed. The result is **streamed** back to the client, parsed by React, and the view is updated with the new todo. ```tsx title="@/pages/todos/AddTodo.tsx" "use client"; // [!code highlight] import { addTodo } from "./functions"; // [!code highlight] export default function AddTodo() { return (
); } ``` ### `serverQuery` and `serverAction` [#serverquery-and-serveraction] Standard React Server Action calls typically expect the server to return the entire updated UI tree so the client can rehydrate the page. For many interactions—especially queries where you only need the returned data—this is unnecessary overhead. To give you more control over this behavior, RedwoodSDK provides `serverQuery` and `serverAction` wrappers. #### `serverQuery` [#serverquery] Use `serverQuery` for fetching data. * **Method**: GET (default). * **Behavior**: Returns data only. Does **not** rehydrate or re-render the page. * **Location**: Must be in a `"use server"` file. We recommend `queries.ts`. ```tsx title="queries.ts" "use server"; import { serverQuery } from "rwsdk/worker"; import { isAuthenticated } from "@/lib/auth"; // Hypothetical auth utility // Simple query export const getTodos = serverQuery(async (userId: string) => { return db.todo.findMany({ where: { userId } }); }); // Query with middleware (e.g. auth check) export const getSecretData = serverQuery([ async () => { // Check auth if (!isAuthenticated()) { throw new Response("Unauthorized", { status: 401 }); } }, async () => { return "Secret Data"; } ]); ``` #### `serverAction` [#serveraction] Use `serverAction` for mutations. * **Method**: POST (default). * **Behavior**: Rehydrates and re-renders the page with the updated server state. * **Location**: Must be in a `"use server"` file. We recommend `actions.ts`. ```tsx title="actions.ts" "use server"; import { serverAction } from "rwsdk/worker"; import { isAuthenticated } from "@/lib/auth"; // Hypothetical auth utility export const createTodo = serverAction(async (title: string) => { await db.todo.create({ data: { title } }); }); // Action with middleware const requireAuth = async () => { // Auth check if (!isAuthenticated()) { throw new Response("Unauthorized", { status: 401 }); } } export const deleteTodo = serverAction([ requireAuth, async (id: string) => { await db.todo.delete({ where: { id } }); } ]); // You can also customize the HTTP method export const searchTodos = serverAction( async (query: string) => { // ... }, { method: "GET" } ); ``` ### How it works [#how-it-works] Behind the scenes, `serverQuery` uses a specialized optimization of the RSC protocol to enable fast, data-only fetches without the overhead of a full page re-render. #### The `x-rsc-data-only` Header [#the-x-rsc-data-only-header] Standard React Server Action calls typically expect the server to return the entire updated UI tree so the client can rehydrate the page. For queries where you only need the returned data, this is unnecessary overhead. When you call a function wrapped in `serverQuery`, the client sends a special `x-rsc-data-only: true` header. #### Server-Side Optimization [#server-side-optimization] The RedwoodSDK server recognizes this header and skips the expensive process of rendering your Page components. Instead, it returns a minimal RSC payload: 1. **`node: null`**: Tells React there is no UI change required. 2. **`actionResult`**: Contains the actual data returned by your function. This allows RedwoodSDK to resolve the function call result directly in your client component while keeping the current UI state perfectly intact, avoiding any flickering or unnecessary hydration cycles. ### Context [#context] Context is a way to share data globally between server components on a per-request basis. The context is populated by middleware, and is available to all React Server Components Pages and Server Functions via the `ctx` prop or `requestInfo.ctx`. ### Returning Responses [#returning-responses] Server Functions can return standard `Response` objects, which is particularly useful for performing redirects or setting custom headers after an action completes. When a `Response` is returned, RedwoodSDK automatically handles it: * **Redirects:** If the response has a 3xx status code and a `Location` header, the client will automatically redirect. * **Custom Responses:** Other response types are also supported, and their metadata (status, headers) is made available on the client. ```tsx title="src/pages/todos/functions.tsx" "use server"; export async function addTodo(formData: FormData) { // ... logic to add todo ... // Redirect to the todos list page after success return Response.redirect("/todos", 303); // [!code highlight] } ``` #### Intercepting Action Responses [#intercepting-action-responses] You can intercept action responses on the client by providing an `onActionResponse` callback to `initClient`. This is useful if you want to handle redirects manually or perform side effects based on the response. ```tsx title="src/entry.client.tsx" // [!code word:onActionResponse] import { initClient } from "rwsdk/client"; initClient({ onActionResponse: (response) => { console.log("Action returned status:", response.status); // Return true to prevent the default redirect behavior // return true; }, }); ``` ## Advanced usage [#advanced-usage] ### Manual rendering [#manual-rendering] RedwoodSDK also provides a way to render your React Server Components imperatively with `renderToStream()` and `renderToString()`. To render your component tree to a [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) ### `renderToStream()` and `renderToString()` [#rendertostream-and-rendertostring] #### `renderToStream(element[, options]): Promise` [#rendertostreamelement-options-promisereadablestream-] Takes in a React Server Component (can be a client component or server component), and returns a stream that decodes to html. ```tsx const stream = await renderToStream(, { Document }) const response = new Response(stream, { status: 404, }); ``` #### Options [#options] * `Document`: The [document](/core/routing/#documents) component to wrap around the React Server Component `element`. If not given, will return the rendered React Server Component without any wrapping. * `injectRSCPayload = false`: Whether to inject the corresponding RSC payload for the React Server Component to use for client-side hydration * `onError`: A callback function called with the relevant error as the only paramter if any errors happen during rendering #### `renderToString(element[, options]): Promise` [#rendertostringelement-options-promisestring-] Takes in a React Server Component (can be a client component or server component), and returns an html string. ```tsx const html = await renderToString(, { Document }) const response = new Response(html, { status: 404, }); ``` #### Options [#options-1] * `Document`: The [document](/core/routing/#documents) component to wrap around the React Server Component `element`. If not given, will return the rendered React Server Component without any wrapping. * `injectRSCPayload = false`: Whether to inject the corresponding RSC payload for the React Server Component to use for client-side hydration --- # Request Handling & Routing (https://docs.rwsdk.com/core/routing) The request/response paradigm is at the heart of web development - when a browser makes a request, your server needs to respond with content. RedwoodSDK makes this easy with the `defineApp` function, which lets you elegantly handle incoming requests and return the right responses. ```tsx title="src/worker.tsx" import { defineApp } from "rwsdk/worker"; import { route } from "rwsdk/router"; import { env } from "cloudflare:workers"; export default defineApp([ // Middleware function middleware({ request, ctx }) { /* Modify context */ }, function middleware({ request, ctx }) { /* Modify context */ }, // Request Handlers route("/", function handler({ request, ctx }) { return new Response("Hello, world!"); }), route("/ping", function handler({ request, ctx }) { return new Response("Pong!"); }), route("/api/users", { get: () => new Response(JSON.stringify(users)), post: () => new Response("Created", { status: 201 }), }), ]); ``` *** The `defineApp` function takes an array of middleware and route handlers that are executed in the order they are defined. In this example the request is passed through two middleware functions before being "matched" by the route handlers. *** ## Matching Patterns [#matching-patterns] Routes are matched in the order they are defined. You define routes using the `route` function. Trailing slashes are optional and normalized internally. ```tsx title="src/worker.tsx" import { route } from "rwsdk/router"; defineApp([route("/match-this", () => new Response("Hello, world!"))]); // [!code highlight] ``` *** `route` parameters: 1. The matching pattern string 2. The request handler function *** There are three matching patterns: #### Static [#static] Match exact pathnames. ```tsx route("/", ...) route("/about", ...) route("/contact", ...) ``` #### Parameter [#parameter] Match dynamic segments marked with a colon (`:`). The values are available in the route handler via `params` (`params.id` and `params.groupId`). ```tsx route("/users/:id", ...) route("/users/:id/edit", ...) route("/users/:id/addToGroup/:groupId", ...) ``` #### Wildcard [#wildcard] Match all remaining segments after the prefix, the values are available in the route handler via `params.$0`, `params.$1`, etc. ```tsx route("/files/*", ...) route("/files/*/preview", ...) route("/files/*/download/*", ...) ``` *** ## Query Parameters [#query-parameters] RedwoodSDK uses the standard Web [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. To access query parameters, you can use the standard `URL` API: ```tsx route("/search", ({ request }) => { const url = new URL(request.url); const name = url.searchParams.get("name"); return
Hello, {name}!
; }); ``` To get multiple values for a single key (e.g., `?tag=js&tag=react`): ```tsx route("/posts", ({ request }) => { const url = new URL(request.url); const tags = url.searchParams.getAll("tag"); // ["js", "react"] return
Filtering by tags: {tags.join(", ")}
; }); ``` *** ## Request Handlers [#request-handlers] The request handler is a function, or array of functions (See [Interrupters](#interrupters)), that are executed when a request is matched. ```tsx title="src/worker.tsx" import { route } from "rwsdk/router"; defineApp([ route("/a-standard-response", ({ request, params, ctx }) => { // [!code highlight:6] return new Response("Hello, world!"); }), route("/a-jsx-response", () => { return
Hello, JSX world!
; }), ]); ``` *** The request handler function takes a [RequestInfo](#request-info) object as its parameter. Return values: * `Response`: A standard response object. * `JSX`: A React component, which is rendered to HTML on the server and **streamed** to the client. This allows the browser to progressively render the page before it is hydrated on the client side. *** ## HTTP Method Routing [#http-method-routing] You can handle different HTTP methods (GET, POST, PUT, DELETE, etc.) on the same path by passing an object with method keys: ```tsx route("/api/users", { get: () => new Response(JSON.stringify(users)), post: ({ request }) => new Response("User created", { status: 201 }), delete: () => new Response("User deleted", { status: 204 }), }); ``` Method handlers can also be arrays of functions, allowing you to use [interrupters](#interrupters) per method: ```tsx route("/api/users", { get: [isAuthenticated, () => new Response(JSON.stringify(users))], post: [isAuthenticated, validateUser, createUserHandler], }); ``` **Standard HTTP Methods**: `delete`, `get`, `head`, `patch`, `post`, `put` **Custom Methods**: Use the `custom` key for non-standard methods (case-insensitive): ```tsx route("/api/search", { custom: { report: () => new Response("Report data"), }, }); ``` **Automatic OPTIONS & 405 Support**: By default, OPTIONS requests return `204 No Content` with an `Allow` header, and unsupported methods return `405 Method Not Allowed`. **Configuration**: Disable automatic behaviors: ```tsx route("/api/users", { get: () => new Response("OK"), config: { disableOptions: true, // OPTIONS returns 405 disable405: true, // Unsupported methods fall through to 404 }, }); ``` *** ### Interrupters [#interrupters] Interrupters are an array of functions that are executed in sequence for each matched request. They can be used to modify the request, context, or to short-circuit the response. A typical use-case is to check for authentication on a per-request basis, as an example you're trying to ensure that a specific user can access a specific resource. ```tsx title="src/worker.tsx" collapse={1-2} import { defineApp } from "rwsdk/worker"; import { route } from "rwsdk/router"; import { EditBlogPage } from "src/pages/blog/EditBlogPage"; function isAuthenticated({ request, ctx }) { // [!code highlight:6] // Ensure that this user is authenticated if (!ctx.user) { return new Response("Unauthorized", { status: 401 }); } } defineApp([route("/blog/:slug/edit", [isAuthenticated, EditBlogPage])]); // [!code highlight] ``` *** For the `/blog/:slug/edit` route, the `isAuthenticated` function will be executed first, if the user is not authenticated, the response will be a 401 Unauthorized. If the user is authenticated, the `EditBlogPage` component will be rendered. Therefore the flow is interrupted. The `isAuthenticated` function can be shared across multiple routes. *** ## Middleware & Context [#middleware--context] The context object (`ctx`) is a mutable object that is passed to each request handler, interrupters, and React Server Functions. It's used to share data between the different parts of your application. You populate the context on a per-request basis via Middleware. Middleware runs before the request is matched to a route. You can specify multiple middleware functions, they'll be executed in the order they are defined. ```tsx title="src/worker.tsx" import { defineApp } from "rwsdk/worker"; import { route } from "rwsdk/router"; import { env } from "cloudflare:workers"; defineApp([ // [!code highlight:6] sessionMiddleware, async function getUserMiddleware({ request, ctx }) { if (ctx.session.userId) { ctx.user = await db.user.find({ where: { id: ctx.session.userId } }); } }, route("/hello", [ function ({ ctx }) { if (!ctx.user) { return new Response("Unauthorized", { status: 401 }); } }, function ({ ctx }) { return new Response(`Hello ${ctx.user.username}!`); }, ]), ]); ``` *** The context object: 1. `sessionMiddleware` is a function that is used to populate the `ctx.session` object 2. `getUserMiddleware` is a middleware function that is used to populate the `ctx.user` object 3. `"/hello"` is a an array of route handlers that are executed when "/hello" is matched: * if the user is not authenticated the request will be interrupted and a 401 Unauthorized response will be returned * if the user is authenticated the request will be passed to the next request handler and `"Hello {ctx.user.username}!"` will be returned *** ### Extending App Context Types [#extending-app-context-types] To get full type safety for your custom context data (like `ctx.user`), you can extend the `DefaultAppContext` interface in a `global.d.ts` file in your project's root. ```typescript title="global.d.ts" import { User } from "@db/index"; import { DefaultAppContext } from "rwsdk/worker"; interface AppContext { user?: User; session?: { userId: string | null }; } declare module "rwsdk/worker" { interface DefaultAppContext extends AppContext {} } ``` Now, whenever you access `ctx` in your handlers or via `getRequestInfo().ctx`, TypeScript will know about the `user` and `session` properties without needing manual casting. *** ## Documents [#documents] Documents are how you define the "shell" of your application's html: the ``, ``, `` tags, scripts, stylesheets, ``, and where in the `` your actual page content is rendered. In RedwoodSDK, you tell it which document to use with the `render()` function in `defineApp`. In other words, you're asking RedwoodSDK to "render" the document. ```tsx title="src/worker.tsx" // [!code word:Document] import { defineApp } from "rwsdk/worker"; import { route, render } from "rwsdk/router"; import { Document } from "@/pages/document"; import { HomePage } from "@/pages/home-page"; export default defineApp([render(Document, [route("/", HomePage)])]); ``` *** The `render` function takes a React component and an array of route handlers. The document will be applied to all the routes that are passed to it. This component will be rendered on the server side when the page loads. When defining this component, you'd add: * Your application's stylesheets and scripts *** ```tsx title="src/pages/document.tsx" export const Document = ({ children }) => ( // [!code highlight:1] {children} ); ``` ## Request Info [#request-info] The `requestInfo` object and `getRequestInfo()` function are available in server functions and provide access to the current request's context. Import them from `rwsdk/worker`: ```tsx import { requestInfo, getRequestInfo } from "rwsdk/worker"; export async function myServerFunction() { // Option 1: Using the requestInfo object const { request, response, ctx } = requestInfo; // Option 2: Using the getRequestInfo() function (recommended for actions) const info = getRequestInfo(); // info.request, info.response, info.ctx.user } ``` *** The `requestInfo` object contains: * `request`: The incoming HTTP [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object * `response`: A [ResponseInit](https://fetch.spec.whatwg.org/#responseinit) object used to configure the status and headers of the response * `ctx`: The app context (same as what's passed to components) * `rw`: RedwoodSDK-specific context * `cf`: Cloudflare's Execution Context API You can mutate the `response` object to configure the status and headers. For example: ```tsx import { requestInfo } from "rwsdk/worker"; import { route } from "rwsdk/router"; const NotFound = () =>
Not Found
; export default defineApp([ route("/some-resource", async () => { // some logic to determine if the resource is not found response.status = 404; response.headers.set("Cache-Control", "no-store"); return ; }), ]); ``` *** ## Generating Links [#generating-links] Use `linkFor` to derive a strongly typed helper from your `defineApp` export. The helper works anywhere you can import types, including client code, because the call only depends on the app type. ```ts title="src/app/shared/links.ts" import { linkFor } from "rwsdk/router"; // Recommended: type-only import to avoid bundling worker code import type * as Worker from "../../worker"; type App = typeof Worker.default; export const link = linkFor(); ``` `linkFor` exposes the full set of routes discovered inside `defineApp`. TypeScript verifies that the path you pass exists and that you provide the required parameters. Using a type-only import ensures bundlers do not include the worker code in client bundles while preserving full types. ### When using Cron, Queues, etc. [#when-using-cron-queues-etc] When using `ExportedHandler` to support Cron, Queues, etc. you need to export the `app` object using the `defineApp` function. ```tsx title="src/worker.tsx" export const app = defineApp([ // <-- Note: `export const app = ...`> setCommonHeaders(), ({ ctx }) => { // setup ctx here ctx; }, render(Document, [route("/", Home)]), ]); export default { fetch: app.fetch, } satisfies ExportedHandler; ``` Then you can use the `link` function to generate links to your routes, but instead of `default` you need to pass the exported `app` object. ```tsx title="src/app/shared/links.ts" import { linkFor } from "rwsdk/router"; // Recommended: type-only import to avoid bundling worker code import type * as Worker from "../../worker"; type App = typeof Worker.app; // <-- Note: `.app`> export const link = linkFor(); ``` ### Examples [#examples] ```tsx title="Anywhere in your app (client or server)" import { link } from "@/shared/links"; // Static route const accountsHref = link("/accounts"); // View Accounts // Dynamic route with params (fully typed) const callDetailsHref = link("/calls/details/:id", { id: call.id }); // View Call // Building currentPath for list pages or search components const currentPath = link("/settings/users"); ``` * Typing `link("` in your editor will autocomplete all valid route patterns from your app. * Passing a path that does not exist in your route tree, or omitting required params, produces a compile-time type error. ### Prefetching pages with `` [#prefetching-pages-with-link-relx-prefetch] When client-side navigation is enabled via `initClientNavigation`, you can hint future navigations using the browser's [Cache API](https://developer.mozilla.org/en-US/docs/Web/API/Cache): ```tsx title="In a route or layout component (React 19)" import { link } from "@/shared/links"; export function AboutPageLayout() { const aboutHref = link("/about/"); return ( <> {/* React 19 will hoist this into */} {/* ...rest of your page... */} ); } ``` After each client navigation, RedwoodSDK scans `link[rel="x-prefetch"][href]` elements and issues background `GET` requests for those route-like URLs with the `__rsc` query parameter set and an `x-prefetch: true` header. Successful responses are stored in a generation-based `Cache` using `cache.put`, following the semantics described in the MDN Cache documentation. When navigating to a prefetched route, the cached response is used instead of making a network request, improving navigation performance. Cache entries are automatically evicted after each navigation to ensure fresh content, using a generation-based pattern that avoids races with in-flight prefetches and isolates cache entries per browser tab. ### Migration tips (from route constants) [#migration-tips-from-route-constants] If you previously used route constant helpers (e.g. `ROUTES` or `ADMIN_ROUTES`), you can migrate incrementally: ```ts // Before // ROUTES.CALLS.INDEX // ROUTES.CALLS.DETAILS(id) // ADMIN_ROUTES.COMPANIES.USERS(id) // After link("/calls"); link("/calls/details/:id", { id }); link("/admin/companies/:id/users", { id }); ``` Notes: * The accepted patterns are exactly those declared in your route tree in `@/worker`. * For routes with optional query strings, build them as needed: ```ts const base = link("/admin/companies/calls/details/:id", { id: callId }); const href = companyId ? `${base}?companyId=${companyId}` : base; ``` --- # Security (https://docs.rwsdk.com/core/security) ## 🔐 Security Headers [#-security-headers] Security headers are an important part of protecting your application from common attacks like cross-site scripting (XSS), clickjacking, and data injection. RedwoodSDK makes it easy to add these headers to your responses using middleware. Here is an example of a middleware that adds a set of common security headers: ```typescript title="src/app/headers.ts" import type { RouteMiddleware } from "rwsdk/worker"; export const setCommonHeaders = (): RouteMiddleware => ({ response, rw: { nonce } }) => { const headers = response.headers; headers.set("X-Frame-Options", "DENY"); headers.set("X-Content-Type-Options", "nosniff"); headers.set("Referrer-Policy", "strict-origin-when-cross-origin"); headers.set( "Content-Security-Policy", `default-src 'self'; script-src 'self' 'nonce-${nonce}'; style-src 'self' 'unsafe-inline'; object-src 'none';` ); headers.set( "Permissions-Policy", "geolocation=(), microphone=(), camera=()" ); }; ``` You can then apply this middleware in your `src/worker.tsx`: ```typescript title="src/worker.tsx" import { rwsdk } from "rwsdk/worker"; import { setCommonHeaders } from "./app/headers.js"; import { routes } from "./app/pages/routes.js"; export default { async fetch(request, env, ctx) { return rwsdk(request, env, ctx, { routes, middleware: [setCommonHeaders()], }); }, }; ``` ### Changing CSP (Content Security Policy) headers [#changing-csp-content-security-policy-headers] Sometimes you need to allow additional resources or modify the Content Security Policy (CSP) to accommodate third-party scripts, styles, or other assets. The CSP headers control what resources can be loaded and executed by your application. #### Adding trusted domains [#adding-trusted-domains] ```diff // In app/headers.ts headers.set( "Content-Security-Policy", - `default-src 'self'; script-src 'self' 'nonce-${nonce}'; style-src 'self' 'unsafe-inline'; object-src 'none';`, + `default-src 'self'; script-src 'self' 'nonce-${nonce}' https://trusted-scripts.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' https://images.example.com; object-src 'none';`, ); ``` #### Allowing images from multiple sources [#allowing-images-from-multiple-sources] When working with images in your RedwoodSDK application, you may need to load images from different sources such as remote URLs or data URIs. The default CSP configuration doesn't include an `img-src` directive, which means images from external sources will be blocked. To allow images from remote URLs and data URIs, add the `img-src` directive to your CSP: ```diff // In app/headers.ts headers.set( "Content-Security-Policy", - `default-src 'self'; script-src 'self' 'nonce-${nonce}'; style-src 'self' 'unsafe-inline'; object-src 'none';`, + `default-src 'self'; script-src 'self' 'nonce-${nonce}'; style-src 'self' 'unsafe-inline'; img-src 'self' https://trusted-images.example.com data:; object-src 'none';`, ); ``` This configuration allows: * `'self'` - Images from your own domain * `https://trusted-images.example.com` - Images from specific trusted domains * `data:` - Data URIs (base64 encoded images) #### Using `nonce` for inline scripts [#using-nonce-for-inline-scripts] Sometimes you need to include inline scripts in your application, but Content Security Policy (CSP) blocks them by default for security reasons. RedwoodSDK automatically generates a fresh, cryptographically secure nonce value for each request You can access this nonce in document or page components rendered by the [router](./routing), using `rw.nonce`. ```tsx export const Document = ({ rw, children }) => ( {children} ); ``` ### Lifting device permission restrictions [#lifting-device-permission-restrictions] Sometimes you need to allow your web application to access device features like the camera, microphone, or geolocation. These permissions are controlled by the `Permissions-Policy` header. To enable device access, you'll need to modify the `Permissions-Policy` header in your `app/headers.ts` file: ```diff // In app/headers.ts headers.set( "Permissions-Policy", - "geolocation=(), microphone=(), camera=()", + "geolocation=self, microphone=self, camera=self" ); ``` The `self` keyword allows the feature to be used only by your own domain. For a complete reference, see the [MDN Permissions Policy documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy). --- # Storage (https://docs.rwsdk.com/core/storage) [Cloudflare R2](https://developers.cloudflare.com/r2/) is an object storage solution that's S3 compatible, global, scalable, and can be used to store files, images, videos, and more! It integrates natively with Cloudflare workers, and therefore, with Redwood. It is available locally during development, and is automatically configured when you deploy to Cloudflare. ## Setup [#setup] To use R2 in your project, you need to create a R2 bucket, and bind it to your worker. ```bash withOutput npx wrangler r2 bucket create my-bucket Creating bucket 'my-bucket'... ✅ Created bucket 'my-bucket' with default storage class of Standard. Configure your Worker to write objects to this bucket: { "r2_buckets": [ { "bucket_name": "my-bucket", "binding": "R2", }, ], } ``` This will create a bucket called `my-bucket`, which you'll have to bind to your worker, which you do by pasting the above into your `wrangler.jsonc` file. ```jsonc title="wrangler.jsonc" { "r2_buckets": [ { "bucket_name": "my-bucket", "binding": "R2", }, ], } ``` After updating `wrangler.jsonc`, run `pnpm generate` to update the generated type definitions. This will make the `my-bucket` bucket available via the `env.R2` binding in your worker. You can then use this binding to upload, download, and manage files stored in R2 using the standard R2 API. ### Naming [#naming] Bucket names must begin and end with an alphanumeric and can only contain letters (a-z), numbers (0-9), and hyphens (-). ## Usage [#usage] RedwoodSDK uses the standard Request/Response objects. When uploading files, the data is streamed directly from the client to R2 storage. Similarly, when downloading files, they are streamed directly from R2 to the client. This streaming approach means files are processed in small chunks rather than loading the entire file into memory, making it memory-efficient and suitable for handling large files. ### Uploading Files [#uploading-files] ```tsx title="src/worker.tsx" import { defineApp } from "rwsdk/worker"; import { route } from "rwsdk/router"; import { env } from "cloudflare:workers"; defineApp([ route("/upload/", async ({ request }) => { const formData = await request.formData(); const file = formData.get("file") as File; // Stream the file directly to R2 const r2ObjectKey = `/storage/${file.name}`; await env.R2.put(r2ObjectKey, file.stream(), { httpMetadata: { contentType: file.type, }, }); return new Response(JSON.stringify({ key: r2ObjectKey }), { status: 200, headers: { "Content-Type": "application/json", }, }); }), ]); ``` ### Downloading Files [#downloading-files] ```tsx title="src/worker.tsx" import { defineApp } from "rwsdk/worker"; import { route } from "rwsdk/router"; import { env } from "cloudflare:workers"; defineApp([ route("/download/*", async ({ request, params }) => { const object = await env.R2.get("/storage/" + params.$0); if (object === null) { return new Response("Object Not Found", { status: 404 }); } return new Response(object.body, { headers: { "Content-Type": object.httpMetadata?.contentType as string, }, }); }), ]); ``` --- # Authentication (https://docs.rwsdk.com/experimental/authentication) ## Passkey Authentication (WebAuthn) [#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 [#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: ```bash # AI: Run this command, then open the INSTRUCTIONS.md file it points to and follow the steps. npx rwsdk addon passkey ``` This 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. --- # Database (https://docs.rwsdk.com/experimental/database) The SDK includes a built-in database solution using **SQLite Durable Objects** and **Kysely** for SQL queries. Create isolated databases at runtime with minimal setup. *** ## Motivation [#motivation] We believe a lightweight, SQL-based query builder is the best fit as the out of the box solution. With just SQL, **you either already know it** (so you can be immediately productive) **or learning it is transferrable knowledge**. This doesn't replace your existing ORMs - you're always free to use your preferred database solution where it makes sense. For applications with modular components or add-ons, there's an additional benefit: natural isolation. Each database instance is completely separate, giving you explicit control over how components communicate with each other's data. `rwsdk/db` delivers both simplicity and isolation in one package: Write your migrations, call `createDb()`, and start querying with full type safety. Types are inferred directly from your migrations. *** ## How It Works [#how-it-works] Under the hood, `rwsdk/db` combines: 1. **SQLite Durable Objects** - Each database instance runs in its own isolated Durable Object 2. **Kysely** - A lightweight, type-safe SQL query builder with the same API naming and semantics as SQL ### Type Inference [#type-inference] Instead of code generation or handwritten types, we infer your database schema directly from your migrations: ```ts import { type Migrations } from "rwsdk/db"; export const migrations = { "001_initial_schema": { async up(db) { return [ await db.schema .createTable("users") .addColumn("id", "text", (col) => col.primaryKey()) .addColumn("username", "text", (col) => col.notNull().unique()) .execute(), ]; }, }, } satisfies Migrations; // TypeScript automatically knows about your 'users' table and its columns const user = await db.selectFrom("users").selectAll().executeTakeFirst(); ``` ### When Migrations Run [#when-migrations-run] Migrations run when `createDb()` is called. If that happens at the module level (shown in the examples), then: **Development**:. Runs when you start your development server. **Production**: When you deploy with `npm run release`, the deployment process includes an initial request to your application, which triggers migration updates. ### Migration Failures and Rollback [#migration-failures-and-rollback] If a migration’s `up()` function fails, `rwsdk/db` automatically calls the corresponding `down()` function to undo any partial changes. This rollback is per-migration - previously successful ones are not affected. Because SQLite doesn’t support transactional DDL (Data Definition Language) statements, a failed migration can leave the database in a partially modified state. It is therefore important to write `down()` functions that are idempotent and can run safely even if `up()` only partially succeeded. ```ts // Example of a defensive down() function async down(db) { // Defensively drop tables that might not exist if `up()` failed await db.schema.dropTable("posts").ifExists().execute(); await db.schema.dropTable("users").ifExists().execute(); } ``` *** ## Setup [#setup] You'll need to create three files and update your Wrangler configuration: ### 1. Define Your Migrations [#1-define-your-migrations] ```ts title="src/db/migrations.ts" import { type Migrations } from "rwsdk/db"; export const migrations = { "001_initial_schema": { async up(db) { return [ await db.schema .createTable("todos") .addColumn("id", "text", (col) => col.primaryKey()) .addColumn("text", "text", (col) => col.notNull()) .addColumn("completed", "integer", (col) => col.notNull().defaultTo(0), ) .addColumn("createdAt", "text", (col) => col.notNull()) .execute(), ]; }, async down(db) { await db.schema.dropTable("todos").ifExists().execute(); }, }, } satisfies Migrations; ``` ### 2. Create Your Database Instance [#2-create-your-database-instance] ```ts title="src/db/index.ts" import { env } from "cloudflare:workers"; import { type Database, createDb } from "rwsdk/db"; import { type migrations } from "@/db/migrations"; export type AppDatabase = Database; export type Todo = AppDatabase["todos"]; export const db = createDb( env.DATABASE, "todo-database", // unique key for this database instance ); ``` ### 3. Create Your Durable Object Class [#3-create-your-durable-object-class] ```ts title="src/db/durableObject.ts" import { SqliteDurableObject } from "rwsdk/db"; import { migrations } from "@/db/migrations"; export class Database extends SqliteDurableObject { migrations = migrations; } ``` ### 4. Export from Worker [#4-export-from-worker] ```ts title="src/worker.tsx" export { Database } from "@/db/durableObject"; // ... rest of your worker code ``` ### 5. Configure Wrangler [#5-configure-wrangler] ```jsonc title="wrangler.jsonc" { "durable_objects": { "bindings": [ { "name": "DATABASE", "class_name": "Database", }, ], }, "migrations": [ { "tag": "v1", "new_sqlite_classes": ["Database"], }, ], } ``` After updating `wrangler.jsonc`, run `pnpm generate` to update the generated type definitions. Ensure `src/db/index.ts`, the Durable Object export in `src/worker.tsx`, and the Wrangler configuration all refer to the same binding and class names. The examples use `Database`. *** ## Usage Examples [#usage-examples] ### Basic CRUD Operations [#basic-crud-operations] ```ts import { db } from "@/db"; // Create a todo const todo = { id: crypto.randomUUID(), text: "Finish the documentation", completed: 0, createdAt: new Date().toISOString(), }; await db.insertInto("todos").values(todo).execute(); // Find a todo const foundTodo = await db .selectFrom("todos") .selectAll() .where("id", "=", todo.id) .executeTakeFirst(); // Update a todo await db .updateTable("todos") .set({ completed: 1 }) .where("id", "=", todo.id) .execute(); // Delete a todo await db.deleteFrom("todos").where("id", "=", todo.id).execute(); ``` ### Complex Queries with Joins [#complex-queries-with-joins] While the guestbook example is simple, you can still perform joins. For a more detailed example, see the **Patterns** section below. ### Real-World Example: Passkey Authentication [#real-world-example-passkey-authentication] Here's how the [passkey addon](https://github.com/redwoodjs/passkey-addon) uses `rwsdk/db`: ```ts // Create a new credential export async function createCredential( credential: Omit, ): Promise { const newCredential: Credential = { id: crypto.randomUUID(), createdAt: new Date().toISOString(), ...credential, }; await db.insertInto("credentials").values(newCredential).execute(); return newCredential; } // Find credentials for a user export async function getUserCredentials( userId: string, ): Promise { return await db .selectFrom("credentials") .selectAll() .where("userId", "=", userId) .execute(); } ``` *** ## Patterns [#patterns] ### Nesting Relational Data (ORM-like Behavior) [#nesting-relational-data-orm-like-behavior] While `rwsdk/db` uses a query builder instead of a full ORM, you can still structure your query results to include nested relational data. Kysely provides helper functions like `jsonObjectFrom` and `jsonArrayFrom` that make this easy. For this example, we'll switch to a more complex schema involving blog posts and users to better demonstrate joins. **1. The Schema** First, let's assume a schema with `users` and `posts`. ```ts title="src/db/migrations.ts" // Abridged for clarity await db.schema .createTable("users") .addColumn("id", "text", (col) => col.primaryKey()) .addColumn("username", "text", (col) => col.notNull().unique()) .execute(); await db.schema .createTable("posts") .addColumn("id", "text", (col) => col.primaryKey()) .addColumn("title", "text", (col) => col.notNull()) .addColumn("userId", "text", (col) => col.notNull().references("users.id")) .execute(); ``` **2. The Query** With the schema in place, you can write a query to fetch posts and embed the author's information. ```ts title="src/db/queries.ts" import { db } from "@/db"; import { jsonObjectFrom } from "kysely/helpers/sqlite"; export async function getAllPostsWithAuthors() { return await db .selectFrom("posts") .selectAll("posts") .select((eb) => [ jsonObjectFrom( eb .selectFrom("users") .select(["id", "username"]) .whereRef("users.id", "=", "posts.userId"), ).as("author"), ]) .execute(); } ``` **3. The Result** The `getAllPostsWithAuthors` function will return an array of post objects, each with a nested author object: ```json [ { "id": "post-123", "title": "My First Post", "author": { "id": "user-abc", "username": "Alice" } } ] ``` This pattern allows you to fetch complex, nested data structures in a single, efficient query. *** ## Seeding Your Database [#seeding-your-database] For development and testing, you'll often need a consistent set of data. You can create a seed script to populate your database with default values. ### 1. Create a Seed Script [#1-create-a-seed-script] Create a script that exports an async function as the default export. This script will have access to your application's environment, including your Durable Object bindings, when run via `rwsdk worker-run`. ```ts title="src/scripts/seed.ts" import { db } from "@/db"; export default async () => { console.log("… Seeding todos"); await db.deleteFrom("todos").execute(); await db .insertInto("todos") .values([ { id: crypto.randomUUID(), text: "Write the seed script", completed: 1, createdAt: new Date().toISOString(), }, { id: crypto.randomUUID(), text: "Update the documentation", completed: 0, createdAt: new Date().toISOString(), }, ]) .execute(); console.log("✔ Finished seeding todos 🌱"); }; ``` ### 2. Add a `seed` script to `package.json` [#2-add-a-seed-script-to-packagejson] Add a script to your `package.json` to run your seed file using the `rwsdk worker-run` command. ```json title="package.json" { "scripts": { "seed": "rwsdk worker-run ./src/scripts/seed.ts" } } ``` ### 3. Run the Seed Script [#3-run-the-seed-script] Now you can seed your database from the command line: ```bash npm run seed ``` *** ## API Reference [#api-reference] ### `createDb()` [#createdb] Creates a database instance connected to a Durable Object. ```ts createDb(durableObjectNamespace: DurableObjectNamespace, key: string): Database ``` * `durableObjectNamespace`: Your Durable Object binding from the environment * `key`: Unique identifier for this database instance * Returns: Kysely database instance with your inferred types ### `Database` Type [#databaset-type] The main database type that provides access to your tables and their schemas. ```ts type AppDatabase = Database; type Todo = AppDatabase["todos"]; // Inferred table type ``` ### `Migrations` Type [#migrations-type] Use to define the structure for your database migrations. ```ts export const migrations = { "001_create_todos": { async up(db) { await db.schema .createTable("todos") .addColumn("id", "text", (col) => col.primaryKey()) .addColumn("text", "text", (col) => col.notNull()) .addColumn("completed", "integer", (col) => col.notNull().defaultTo(0)) .execute(); }, async down(db) { await db.schema.dropTable("todos").execute(); }, }, } satisfies Migrations; ``` ### `SqliteDurableObject` [#sqlitedurableobject] Base class for your Durable Object that handles SQLite operations. ```ts class YourDurableObject extends SqliteDurableObject { migrations = yourMigrations; } ``` For complete query builder documentation, see the [Kysely documentation](https://kysely.dev/docs). Everything you can do with Kysely, you can do with `rwsdk/db`. *** ## FAQ [#faq] **Q: Why use SQL instead of an ORM?** A: We're not replacing ORMs - `rwsdk/db` works alongside your existing tools. We believe a lightweight, SQL-based query builder is a better fit as the out of the box solution, but you're always free to use your preferred ORM or database solution where it makes sense for your application. **Q: What about latency and performance?** A: Durable Objects run in a single location, so there's a latency consideration compared to globally distributed databases. However, they excel at simplicity and isolation. For many use cases, the ease of setup benefit outweighs the latency trade-off. You can also create multiple database instances with different keys to distribute load geographically if needed. **Q: Is this suitable for production use?** A: This is currently a preview feature, which means the API may evolve based on feedback. The underlying technologies (SQLite, Durable Objects, Kysely) are all production-ready, but we recommend testing thoroughly and having migration strategies ready as the API stabilizes. **Q: How do I handle database backups?** A: Durable Objects automatically persist data, but like D1, there aren't built-in backup features. For critical applications, implement additional backup strategies. You can export data periodically or replicate to external systems as needed. **Q: Why does rwsdk/db auto-rollback failed migrations instead of leaving recovery to the developer?** A: We recognize that in many scenarios, particularly in production, a developer is best equipped to handle a failed migration with full context. Manual recovery can offer more granular control than a one-size-fits-all automated approach. However, `rwsdk/db` opts for automated rollbacks by default to ensure database integrity. The primary reason is that SQLite does not support transactions for schema changes (DDL). A failed `up()` migration could otherwise leave the database in an inconsistent, half-migrated state. By automatically running the `down()` function, we return the database to a known-good state. This is critical for the zero-setup, runtime-isolated environments `rwsdk/db` is designed for, where direct manual intervention may not be feasible. To work effectively with this automated system, it's best to plan migrations carefully. Each `down()` function should be written to cleanly undo only what its corresponding `up()` function does. This practice makes it much easier and safer to reason about the database state, fix the migration, and redeploy. --- # Realtime (https://docs.rwsdk.com/experimental/realtime) RedwoodSDK provides built-in support for real-time applications through shared state synchronization. The primary way to implement this is via the `useSyncedState` hook. `useSyncedState` looks exactly like React's native `useState`, except it has bidirectional syncing with the server and all other connected clients. ## What is it? [#what-is-it] * It's a hook that synchronizes state across multiple clients (tabs, devices, users) in real-time. * The server is the source of truth. * It allows you to build collaborative features without needing an external realtime service. ## Why would you use it? [#why-would-you-use-it] * **Realtime**: Updates are instant for all users on the page. * **Native**: It's built into RedwoodSDK. * **Cloudflare**: It leverages Cloudflare Durable Objects for coordination. ## Where would you use it? [#where-would-you-use-it] * Any component where you want data to update instantly for everyone. * Examples: Chat apps, collaborative forms, live dashboards, presence indicators. ## Tutorial: From 0 to 1 [#tutorial-from-0-to-1] Here is the easiest way to get started. ### 1. Setup the Worker [#1-setup-the-worker] In your `src/worker.tsx`, you need to export the `SyncedStateServer` (the Durable Object) and register its routes. ```tsx title="src/worker.tsx" import { env } from "cloudflare:workers"; import { SyncedStateServer, syncedStateRoutes, } from "rwsdk/use-synced-state/worker"; import { defineApp } from "rwsdk/worker"; // 1. Export the Durable Object so Cloudflare can find it export { SyncedStateServer }; export default defineApp([ // ... your other middleware // 2. Register the synced state routes ...syncedStateRoutes(() => env.SYNCED_STATE_SERVER), ]); ``` ### 2. Update Wrangler Config [#2-update-wrangler-config] You need to tell Cloudflare about the Durable Object. Add the following to your `wrangler.jsonc`: ```jsonc title="wrangler.jsonc" "durable_objects": { "bindings": [ { "name": "SYNCED_STATE_SERVER", "class_name": "SyncedStateServer" } ] }, "migrations": [ { "tag": "v1", "new_sqlite_classes": ["SyncedStateServer"] } ] ``` > **Note**: After changing `wrangler.jsonc`, run `pnpm generate` to update your types. ### 3. Use the Hook [#3-use-the-hook] Now you can use `useSyncedState` in your components. It works just like `useState`, but takes a second argument: a unique key, and an optional third argument: a room ID. ```tsx title="src/components/SharedCounter.tsx" "use client"; import { useSyncedState } from "rwsdk/use-synced-state/client"; export const SharedCounter = () => { // "counter" is the unique key for this piece of state // Without a room ID, this state is global across all clients const [count, setCount] = useSyncedState(0, "counter"); return (

Count: {count}

); }; ``` Open this component in two different browser windows. When you click increment in one, it updates in the other instantly! ### Rooms: Scoping State to Different Groups [#rooms-scoping-state-to-different-groups] By default, state is global. But you can scope state to different "rooms" by passing a room ID as the third argument. This is useful for features like chat rooms, game sessions, or collaborative documents. ```tsx title="src/components/RoomCounter.tsx" "use client"; import { useSyncedState } from "rwsdk/use-synced-state/client"; export const RoomCounter = ({ roomId }: { roomId: string }) => { // State is scoped to this specific room // Users in different rooms won't see each other's updates const [count, setCount] = useSyncedState(0, "counter", roomId); return (

Room: {roomId}

Count: {count}

); }; ``` When you use a room ID, state is isolated to that room. Users in `"room-1"` won't see updates from users in `"room-2"`. *** ## Advanced: Scoping and Persistence [#advanced-scoping-and-persistence] ### Scoping State with Room IDs vs Key Handlers [#scoping-state-with-room-ids-vs-key-handlers] There are two ways to scope state: 1. **Room IDs** (client-side): Pass a room ID as the third argument to `useSyncedState`. This is the simplest way to isolate state between different groups. 2. **Key Handlers** (server-side): Transform keys on the server to add prefixes or scoping logic. This is useful when you need server-enforced scoping based on authentication or other server-side data. #### Using Room IDs (Client-Side) [#using-room-ids-client-side] ```tsx title="src/components/ChatRoom.tsx" "use client"; import { useSyncedState } from "rwsdk/use-synced-state/client"; export const ChatRoom = ({ roomId }: { roomId: string }) => { // Each room has its own isolated state const [messages, setMessages] = useSyncedState([], "messages", roomId); // ... chat UI }; ``` #### Using Key Handlers (Server-Side) [#using-key-handlers-server-side] Key handlers allow you to transform keys on the server, which is useful for server-enforced scoping: ```tsx title="src/worker.tsx" import { requestInfo } from "rwsdk/worker"; SyncedStateServer.registerKeyHandler(async (key, stub) => { // Access user ID from request context const userId = requestInfo.ctx.userId; // Scope keys that start with "user:" to the current user if (key.startsWith("user:")) { return `${key}:${userId}`; } return key; }); ``` Then in your component: ```tsx title="src/components/UserSettings.tsx" "use client"; import { useSyncedState } from "rwsdk/use-synced-state/client"; export const UserSettings = () => { // The key handler will transform this to "user:settings:123" (where 123 is the userId) // Each user gets their own isolated settings const [settings, setSettings] = useSyncedState({}, "user:settings"); // ... settings UI }; ``` #### Server-Side Room Transformation [#server-side-room-transformation] You can also transform room IDs on the server using a room handler. This is useful for features like "private" rooms that should be scoped per user: ```tsx title="src/worker.tsx" import { requestInfo } from "rwsdk/worker"; SyncedStateServer.registerRoomHandler(async (roomId, reqInfo) => { const userId = reqInfo?.ctx?.userId; // Transform "private" room requests to user-specific rooms if (roomId === "private" && userId) { return `user:${userId}`; } // Pass through other room IDs as-is return roomId ?? "syncedState"; }); ``` Then clients can request a "private" room, and the server will automatically scope it to the current user: ```tsx title="src/components/PrivateNotes.tsx" "use client"; import { useSyncedState } from "rwsdk/use-synced-state/client"; export const PrivateNotes = () => { // Server transforms "private" to "user:${userId}" automatically const [notes, setNotes] = useSyncedState("", "notes", "private"); // ... notes UI }; ``` ### Persisting State [#persisting-state] Since the state is in-memory, you might want to save it to a database. You can register handlers for when state is set or retrieved. ```tsx title="src/worker.tsx" SyncedStateServer.registerSetStateHandler((key, value) => { console.log("State updated:", key, value); // db.save(key, value); }); SyncedStateServer.registerGetStateHandler((key, value) => { // potentially load from DB if value is undefined }); ``` ## Automatic Reconnection [#automatic-reconnection] `useSyncedState` automatically handles dropped WebSocket connections. If the connection dies (bad wifi, server restart, etc.), it will: 1. Detect the broken connection 2. Reconnect with exponential backoff (1s, 2s, 4s... up to 30s) 3. Re-subscribe all active state keys 4. Fetch the latest state so your UI catches up on missed updates This happens transparently — your components don't need any changes. ### Connection Status Callback [#connection-status-callback] If you want to show a banner or toast when the connection drops, use the `onStatusChange` callback: ```tsx title="src/app/hooks.ts" import { createSyncedStateHook } from "rwsdk/use-synced-state/client"; export const useSyncedState = createSyncedStateHook({ onStatusChange(status) { // status: "connected" | "disconnected" | "reconnecting" if (status === "disconnected") { console.warn("Connection lost, reconnecting..."); } if (status === "connected") { console.log("Connection restored"); } }, }); ``` Then use your custom hook instead of the default: ```tsx title="src/app/components/SharedCounter.tsx" "use client"; import { useSyncedState } from "@/app/hooks"; export const SharedCounter = () => { const [count, setCount] = useSyncedState(0, "counter"); // ... }; ``` ## Future Plans [#future-plans] We are working on making this even more powerful out of the box: * **Offline Support**: Local persistence (e.g., via IndexedDB) so your app works offline and syncs changes when the connection is restored. * **Durable Storage**: Built-in persistence to the Durable Object's SQLite storage, ensuring state survives worker restarts without custom handlers. --- # Quick Start (https://docs.rwsdk.com/getting-started/quick-start) In this quick start you'll go from zero to request/response in seconds and deploy to production in minutes! Create a new project by running the following command, replacing `my-project-name` with your project name: ## Start developing [#start-developing] ### Install the dependencies [#install-the-dependencies] ```bash cd my-project-name ``` ### Run the development server [#run-the-development-server] RedwoodSDK is just a plugin for Vite, so you can use the same commands to run the development server as you would with any other Vite project. ```bash VITE v6.2.0 ready in 500 ms ➜ Local: http://localhost:5173/ ➜ Network: use --host to expose ➜ press h + enter to show help ``` Access the development server in your browser, by default it's available at [http://localhost:5173](http://localhost:5173), where you should see the RedwoodSDK welcome page displayed. How exciting, your first request/response in RedwoodSDK! ### Your first route [#your-first-route] The entry point of your webapp is `src/worker.tsx`, open that file in your favorite editor. Here you'll see the `defineApp` function, this is the main function that "defines your webapp," where the purpose is to handle requests by returning responses to the client. ```tsx title="src/worker.tsx" // [!code word:defineApp] import { defineApp } from "rwsdk/worker"; import { route, render } from "rwsdk/router"; import { Document } from "@/app/document"; import { Home } from "@/app/pages/home"; export default defineApp([ render(Document, [route("/", () => new Response("Hello, World!"))]), ]); ``` You're going to add your own route, insert the `"/ping"` route handler: ```tsx title="src/worker.tsx" import { defineApp } from "rwsdk/worker"; import { route, render } from "rwsdk/router"; export default defineApp([ render(Document, [ route("/", () => new Response("Hello, World!")), // [!code ++:3] route("/ping", function () { return

Pong!

; }), ]), ]); ``` Now when you navigate to [http://localhost:5173/ping](http://localhost:5173/ping) you should see "Pong!" displayed on the page. ## Deploy to production [#deploy-to-production] RedwoodSDK is built for the Cloudflare Development Platform. You can deploy your webapp to Cloudflare with a single command: The first time you run the command it might fail and ask you to create a workers.dev subdomain. Do as indicated and go to the dashboard and open the Workers menu. Opening the Workers landing page for the first time will create a workers.dev subdomain automatically --- # Building with AI (https://docs.rwsdk.com/guides/build-with-ai) RedwoodSDK is designed to be AI-friendly. By following "Zero Magic" principles and staying close to web standards, it ensures that what you see in your source code is exactly what runs in the browser and on the server. This makes RedwoodSDK code highly predictable for AI tools. ## Context files [#context-files] RedwoodSDK provides `llms.txt` and `llms-full.txt` files that contain the full documentation content in a format optimized for AI consumption. These files are located at the root of the documentation site. * [llms.txt](https://docs.rwsdk.com/llms.txt) — A concise summary of the documentation. * [llms-full.txt](https://docs.rwsdk.com/llms-full.txt) — The full documentation content. Some AI tools, like Cursor or Windsurf, can auto-discover these files if you provide `https://docs.rwsdk.com` as a documentation source. ## Tips for AI-Powered Development [#tips-for-ai-powered-development] ### 1. Leverage "Zero Magic" [#1-leverage-zero-magic] Because RedwoodSDK avoids hidden behavior and complex code generation, AI tools are less likely to hallucinate internal framework logic. When asking an AI to write code, emphasize that it should use standard Web APIs (Request, Response, Fetch) and idiomatic React Server Components. ### 2. Use `create-rwsdk` for Scaffolding [#2-use-create-rwsdk-for-scaffolding] Rather than asking an AI to set up a project from scratch, always start with the official starter: ```bash npx create-rwsdk my-project-name ``` ### 3. Server Functions and RSCs [#3-server-functions-and-rscs] AI tools are generally well-versed in React, but they may need reminders about React Server Components (RSC) and Server Functions. If an AI is struggling with data fetching, remind it that it can use `async/await` directly in components or define `"use server";` functions for server-side logic. ### 4. Cloudflare Runtime Aware [#4-cloudflare-runtime-aware] RedwoodSDK runs on the Cloudflare Workers runtime (workerd). When asking for infrastructure help (e.g., using D1, R2, or Durable Objects), remind the AI that these are available via standard Cloudflare bindings. ## Community Support [#community-support] If you're stuck or want to share how you're using AI with RedwoodSDK, join our community: Join the [RedwoodJS Discord](https://community.redwoodjs.com/) to chat with other developers and the core team. --- # Drizzle ORM (https://docs.rwsdk.com/guides/database/drizzle) **Steps for integrating Drizzle/D1 into RWSDK** Create a Cloudflare D1 database: ```bash npx wrangler d1 create your_prod_database_name ``` *** Add database binding to wrangler: ```jsonc // wrangler.jsonc "d1_databases": [ { "binding": "DB", "database_name": "your_prod_database_name", "database_id": "prod_database_id", "migrations_dir": "drizzle" } ], ``` *** Install Drizzle packages: ```bash npm i drizzle-orm npm i -D drizzle-kit ``` *** Create a `drizzle.config.ts` file and fill in like below. Note that in the example below, you only need the `dbCredentials` object IFF you want to access your dev database using Drizzle Studio. Some developers do not use Drizzle Studio and instead use TablePlus, which can access not only your dev database but also your production D1 on Cloudflare. If you won't be using Drizzle Studio then remove the `dbCredentials` object from the example below. ```ts // drizzle.config.ts import { defineConfig } from "drizzle-kit"; export default defineConfig({ schema: "./src/db/schema.ts", out: "drizzle", dialect: "sqlite", dbCredentials: { url: "./path_to_your_dev_database", }, }); ``` *** Update your `package.json` to add in these scripts: ```json "migrate:new": "drizzle-kit generate", "migrate:dev": "wrangler d1 migrations apply DB --local", "migrate:prod": "wrangler d1 migrations apply DB --remote", ``` *** Update your worker entry file: ```ts // src/worker.tsx export interface Env { DB: D1Database; } ``` Run `npx wrangler types` to update your `worker-configuration.d.ts` configuration. *** Create a schema at `/src/db/schema.ts`. Here's a basic one, which includes an implementation for CUIDs ```ts // src/db/schema.ts import { sqliteTable, text, integer, real, AnySQLiteColumn, index } from "drizzle-orm/sqlite-core"; import { relations, sql } from 'drizzle-orm'; let counter = 0; function createId(): string { const timestamp = Date.now().toString(36); counter = (counter + 1) % 1296; const count = counter.toString(36).padStart(2, '0'); const array = new Uint8Array(8); crypto.getRandomValues(array); const random = Array.from(array).map(b => b.toString(36)).join('').slice(0, 14); return `c${timestamp}${count}${random}`.slice(0, 25); } export const users = sqliteTable('users', { id: text('id').primaryKey().$defaultFn(() => createId()), name: text('name').notNull(), email: text('slug').notNull().unique(), createdAt: text('created_at').notNull().default(sql`(datetime('now', 'localtime'))`), updatedAt: text('updated_at').notNull().default(sql`(datetime('now', 'localtime'))`), }); export type User = typeof users.$inferSelect; export type UserInsert = typeof users.$inferInsert; ``` *** Configure app for database usage by creating `src/db/db.ts` as such: ```ts // src/db/db.ts import { drizzle } from "drizzle-orm/d1"; import { env } from "cloudflare:workers"; import * as schema from "./schema"; export const db = drizzle(env.DB, { schema }); ``` *** And here's an example for accessing the database: ```ts // src/app/pages/dashboard.tsx import { db } from "@/db/db"; import { users, type User } from "@/db/schema"; export const Dashboard = async ({ ctx }: { ctx: any }) => { const allUsers: User[] = await db.select().from(users); } ``` *** Before running the app and accessing your local database, you'll need to generate and apply a migration based on your schema: ```bash npm run migrate:new npm run migrate:dev ``` Now you should be able to start the dev server (`npm run dev`) and access the database per your schema. --- # Debugging (https://docs.rwsdk.com/guides/debugging) This guide explains how to set up VS Code or Cursor to debug both your client-side and server-side (worker) code. ## Setup [#setup] For debugging to work, you'll need a `.vscode/launch.json` file in your project. If you created your project with `create-rwsdk`, this file should already be there. If not, create the file and add the following configuration: ```json title=".vscode/launch.json" { "version": "0.2.0", "configurations": [ { "name": "Debug Vite App (Client)", "type": "chrome", "request": "launch", "url": "http://localhost:5173", "webRoot": "${workspaceFolder}", "sourceMaps": true, "skipFiles": ["/**"] }, { "name": "Attach to Worker", "type": "node", "request": "attach", "port": 9229, "address": "localhost", "restart": false, "protocol": "inspector", "skipFiles": ["/**"], "localRoot": "${workspaceFolder}", "remoteRoot": "${workspaceFolder}", "sourceMaps": true }, { "name": "Attach to Worker (Port 9299)", "type": "node", "request": "attach", "port": 9299, "address": "localhost", "restart": false, "protocol": "inspector", "skipFiles": ["/**"], "localRoot": "${workspaceFolder}", "remoteRoot": "${workspaceFolder}", "sourceMaps": true } ] } ``` ## Debugging Server-Side Code (Worker) [#debugging-server-side-code-worker] To debug server-side code, such as server components or server functions: 1. **Start the dev server** in your terminal: ```bash npm run dev ``` 2. **Attach the debugger**: * In VS Code, open the "Run and Debug" panel (Cmd+Shift+D on Mac, Ctrl+Shift+D on Windows). * Select **"Attach to Worker"** from the dropdown and press the play button (F5). * If the terminal shows a message like "Default inspector port 9229 not available, using 9299 instead," use the **"Attach to Worker (Port 9299)"** configuration instead. 3. **Set breakpoints**: Place breakpoints in your server-side code (e.g., in `src/worker.tsx` or a server component). They should now be hit when the code is executed. ## Debugging Client-Side Code [#debugging-client-side-code] 1. Make sure your dev server is already running. 2. In the "Run and Debug" panel, select **"Debug Vite App (Client)"** and press F5. 3. This will open a new Chrome window. Breakpoints in your client-side code (e.g., components with a `"use client"` directive) will now work. ## Limitations [#limitations] Currently, debugging server-side rendering (SSR) of components is not fully supported. However, you can debug other worker code paths, including server components and server functions, as well as all client-side code. --- # Email Templates (https://docs.rwsdk.com/guides/email/email-templates) The [React Email](https://react.email/) project makes it easy to create email templates. It includes unstyled components and the Tailwind CSS support. ## Installation [#installation] Install the React Email components to your project by running the following command in the Terminal: ```bash pnpm add @react-email/components ``` This assumes that you've already installed `Resend` to your project. If not, you can install the React Email components and Resend in one go: ```bash pnpm add @react-email/components resend ``` Then, install the React Email Preview: ```bash npx create-email@latest ``` This will create a new directory called `react-email-starter`. ```bash cd react-email-starter pnpm install pnpm dev ``` Now, you can open a browser at [http://localhost:3000](http://localhost:3000) and see the preview. In the left sidebar, you'll notice that React Email has 4 email examples you can preview. These correspond with the `.tsx` files inside the `react-email-starter/emails` directory: All the assets for these example emails are within the `react-email-starter/emails/static` directory. You can delete all the contents inside the `emails` directory. Or, oftentimes, I'll create a sub directory called `archive` and move the contents there. ## Creating a New Email Template [#creating-a-new-email-template] 1. Create a new file in the `emails` directory with a `.tsx` extension. 2. Inside your email file, paste the following code: ```tsx title="src/emails/welcome-email.tsx" import { Body, Container, Head, Heading, Html, Preview, } from "@react-email/components"; export default function WelcomeEmail() { return ( Hello World Hello World ); } ``` You'll notice that React Email has several primitives, making it easy to create a new email template. You can see a full list of components on the [React Email documentation](https://react.email/docs/introduction). React Email also has several [prebuilt components](https://react.email/components) for [galleries](https://react.email/components/gallery), [e-commerce](https://react.email/components/ecommerce), [articles](https://react.email/components/articles), etc. 3. Adjust the template to your liking. Keep in mind that you can pass in props, personalizing the template and making it more dynamic. ```tsx title="src/emails/welcome-email.tsx" lineNumbers=10 interface WelcomeEmailProps { name: string; } export default function WelcomeEmail({ name }: WelcomeEmailProps) { return ( Hello {name} Hello {name} ); } ``` 4. When you've finished building the email template, you can get the code by clicking on the Code button in the browser preview. From here, you download or copy and paste the React code, HTML, or Plain Text into your project. Create a new folder inside the `src/app` directory called `emails` and paste the React Email code into the new file. * src/ * app/ * emails/ * WelcomeEmail.tsx 5. Updating your Resend Code: Now, you can update your Resend code (pulled from the example on [Sending Email page](/guides/email/sending-email)) to use the new email template. ```tsx title="src/app/emails/WelcomeEmail.tsx" import WelcomeEmail from "@/app/emails/WelcomeEmail"; const { data, error } = await resend.emails.send({ from: "Acme ", to: email, subject: "👋 Hello World", react: , }); ``` ## Using Tailwind within Email Templates [#using-tailwind-within-email-templates] 1. `import` the Tailwind component at the top of your email template: ```tsx title="src/emails/WelcomeEmail.tsx" import { Tailwind } from "@react-email/components"; ``` 2. Wrap your email template in the `Tailwind` component: ```tsx title="src/emails/WelcomeEmail.tsx" Hello World ``` 3. If you want to use a custom theme, this will need to be defined as a `config` prop, passed into the `Tailwind` component. ```tsx title="src/emails/WelcomeEmail.tsx" ... ``` If you go this route, we recommend putting the `config` prop in a separate file, importing it into your email template, and passing it into the `Tailwind` component. ```tsx title="src/emails/tailwind.config.ts" export default { theme: { extend: { } } } ``` ```tsx title="src/emails/WelcomeEmail.tsx" import tailwindConfig from "./tailwind.config"; ``` ## Further Reading [#further-reading] * [React Email](https://react.email/) * [React Email Components](https://react.email/docs/components/html) * [React Email Pre-Built Components](https://react.email/components) * [React Email and Tailwind CSS Documentation](https://react.email/docs/components/tailwind) --- # Sending Email (https://docs.rwsdk.com/guides/email/sending-email) ## Setting Up Resend [#setting-up-resend] 1. Go to [Resend](https://resend.com/) and click on **Get Started** to create an account. 2. Once you’ve created an account, you’ll be redirected to a page with instructions for sending your first email. Create an API key by clicking on the “Add API Key” button. Copy the API and go to your .env file. Add a variable called RESEND\_API and paste your key: ```bash title=".env" RESEND_API=re_1234567890 ``` Cloudflare uses a `.dev.vars` file for environment variables. But, the common practice is to use a `.env` file. So, we've created a symlink for you. Anytime you make a change to the `.env` file, it will automatically update the `.dev.vars` file. If you're missing the `.dev.vars` file, as soon as you run `pnpm dev`, it will be created for you. 3. Install the Resend package. Within the Terminal, run: ```bash pnpm add resend ``` Your setup is complete! 🥳 Now, we can send email. ## Sending Email [#sending-email] Now, we can send email. ```tsx title="src/app/auth/actions.ts" import { Resend } from "resend"; const resend = new Resend(env.RESEND_API); const { data, error } = await resend.emails.send({ from: "Acme ", to: email, subject: "👋 Hello World", text: `Hello World`, }); ``` When using Resend, you can send `text`, `react`, or `html` emails. ### Example: Sending Text Email [#example-sending-text-email] ```tsx // [!code word:text] const { data, error } = await resend.emails.send({ from: "Acme ", to: email, subject: "👋 Hello World", text: `Hello World`, // [!code highlight] }); ``` ### Example: Sending React Email [#example-sending-react-email] ```tsx // [!code word:react\:] const Email = ({ name }: { name: string }) => { // [!code highlight:3] return
Hello {name}
; }; const { data, error } = await resend.emails.send({ from: "Acme ", to: email, subject: "👋 Hello World", react: , // [!code highlight] }); ``` Resend also backs the [React Email](https://react.email/) project. This library includes unstyled components and the Tailwind CSS support. More under [Email Templates](/guides/email/email-templates). ### Example: Sending HTML Email [#example-sending-html-email] ```tsx // [!code word:html] const { data, error } = await resend.emails.send({ from: "Acme ", to: email, subject: "👋 Hello World", html: "

Hello World

", // [!code highlight] }); ``` ## Test Emails [#test-emails] > Resend provides a set of safe email addresses specifically designed for testing, ensuring that you can simulate different email events without affecting your domain’s reputation. > [Resend Documentation](https://resend.com/docs/knowledge-base/what-email-addresses-to-use-for-testing#list-of-addresses-to-use) A lot of developers will use `@example.com` or `@test.com` for testing. However, these addresses will often reject messages, leading to bounces. A high bounce rate can negatively impact your sender reputation and affect future deliverability. Therefore, Resend will return a `422` error if you attempt to use these addresses. Instead, Resend provides the following addresses: | **Address** | **Delivery Event Simulated** | | ---------------------- | ---------------------------- | | `delivered@resend.dev` | Email was delivered | | `bounced@resend.dev` | Email was bounced | ## Constants File [#constants-file] We recommend creating a constants file to store reusable values. Inside your `src/app/shared` directory, create a new file called `constants.ts`. * src/ * app/ * shared/ * constants.ts Inside the `constants.ts` file, add the following: ```tsx export const CONSTANTS = Object.freeze({ FROM_EMAIL: "Acme ", }); ``` Now, you can use the `FROM_EMAIL` constant in your code. ```ts import { CONSTANTS } from "~/shared/constants"; // [!code highlight] ... const { data, error } = await resend.emails.send({ from: CONSTANTS.FROM_EMAIL, // [!code highlight] to: email, subject: "👋 Hello World", text: `Hello World`, }); ``` ## Further Reading [#further-reading] * [Resend's Official Documentation](https://resend.com/docs/introduction) * [What email addresses to use for testing?](https://resend.com/docs/knowledge-base/what-email-addresses-to-use-for-testing#list-of-addresses-to-use) --- # Ark UI (https://docs.rwsdk.com/guides/frontend/ark-ui) ## Installing Ark UI [#installing-ark-ui] Ark UI is a headless component library that provides unstyled, accessible components powered by state machines. It gives you complete control over styling while handling all the complex behavior and accessibility. 1. Install Ark UI 2. Import and use components Ark UI components follow a namespace pattern. Here's an example with a Dialog: ```tsx title="src/app/pages/Home.tsx" import { Dialog } from "@ark-ui/react/dialog"; import { Portal } from "@ark-ui/react/portal"; export function Home() { return ( Open Dialog Dialog Title This is a dialog description. Close ); } ``` 3. Run development server ## Styling Ark UI Components [#styling-ark-ui-components] Since Ark UI components are headless, you need to style them yourself. Each component part includes `data-scope` and `data-part` attributes for easy targeting. ### Using Tailwind CSS [#using-tailwind-css] If you're using [Tailwind CSS](/guides/frontend/tailwind), you can style components with utility classes: ```tsx title="src/components/ui/dialog.tsx" import { Dialog } from "@ark-ui/react/dialog"; import { Portal } from "@ark-ui/react/portal"; export function StyledDialog({ children }: { children: React.ReactNode }) { return ( Open Dialog Dialog Title Dialog description goes here.
{children}
Cancel
); } ``` ### Using Vanilla CSS [#using-vanilla-css] Alternatively, target components using their data attributes: ```css title="src/app/styles.css" /* Dialog Backdrop */ [data-scope="dialog"][data-part="backdrop"] { position: fixed; inset: 0; background-color: rgba(0, 0, 0, 0.5); backdrop-filter: blur(4px); } /* Dialog Content */ [data-scope="dialog"][data-part="content"] { background-color: white; border-radius: 8px; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); max-width: 28rem; padding: 1.5rem; } /* Dialog Title */ [data-scope="dialog"][data-part="title"] { font-size: 1.5rem; font-weight: 700; margin-bottom: 0.5rem; } ``` ## Component Patterns [#component-patterns] ### Controlled Components [#controlled-components] ```tsx import { Slider } from "@ark-ui/react/slider"; import { useState } from "react"; export function ControlledSlider() { const [value, setValue] = useState([30]); return ( setValue(details.value)} > Volume: {value} ); } ``` ### TypeScript Support [#typescript-support] Ark UI is fully typed. Import types from component namespaces: ```tsx import { Select } from "@ark-ui/react/select"; import type { SelectRootProps } from "@ark-ui/react/select"; interface CustomSelectProps extends SelectRootProps { label: string; options: string[]; } export function CustomSelect({ label, options, ...props }: CustomSelectProps) { return ( {label} {/* Select implementation */} ); } ``` ## Pre-Styled Options [#pre-styled-options] If you want pre-styled Ark UI components instead of building from scratch: * **[Park UI](https://park-ui.com/)** - Ark UI components styled with Panda CSS * **[Tark UI](https://tarkui.com/)** - Ark UI components styled with Tailwind CSS ## Further Reading [#further-reading] * [Ark UI Documentation](https://ark-ui.com/) * [Ark UI Components](https://ark-ui.com/docs/components/accordion) * [Ark UI GitHub](https://github.com/chakra-ui/ark) * [Zag.js State Machines](https://zagjs.com/) --- # Chakra UI (https://docs.rwsdk.com/guides/frontend/chakra-ui) ## Installing Chakra UI [#installing-chakra-ui] Since RedwoodSDK is based on React and Vite, we can work through the ["Using Vite" documentation](https://chakra-ui.com/docs/get-started/frameworks/vite) from Chakra UI. 1. Install Chakra UI 2. Add Component Snippets Chakra UI v3 introduces a snippet-based system that gives you full control over components. Snippets are pre-built component compositions that are copied into your project. ```bash npx @chakra-ui/cli snippet add ``` This command adds the default snippet set and writes files into `src/components/ui/`. You can also add all snippets or choose specific ones. 3. Configure TypeScript Paths Update your `tsconfig.json` to include path mappings for the snippets: ```json title="tsconfig.json" { "compilerOptions": { "target": "ESNext", "module": "ESNext", "moduleResolution": "Bundler", "skipLibCheck": true, // [!code ++:3] "paths": { "@/*": ["./src/*"] } } } ``` For JavaScript projects, create a `jsconfig.json` file with the same configuration. 4. Install Vite TypeScript Paths Plugin To sync your TypeScript paths with Vite, install the `vite-tsconfig-paths` plugin: 5. Configure the Vite Plugin ```ts title="vite.config.mts" import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import tsconfigPaths from "vite-tsconfig-paths"; // [!code ++] import { redwood } from "rwsdk/vite"; import { cloudflare } from "@cloudflare/vite-plugin"; export default defineConfig({ plugins: [ // [!code ++] cloudflare({ viteEnvironment: { name: "worker" }, }), redwood(), react(), tsconfigPaths(), ], }); ``` 6. Set Up the Provider The Chakra UI provider needs to wrap your application. In RedwoodSDK, you'll want to add this to your root component or layout. First, create a provider component if the snippet didn't generate one: ```tsx title="src/components/ui/provider.tsx" import { ChakraProvider, defaultSystem } from "@chakra-ui/react"; import { ColorModeProvider } from "@/components/ui/color-mode"; export function Provider(props: { children: React.ReactNode }) { return ( {props.children} ); } ``` Then wrap your routes with a layout: ```tsx title="src/app/layouts/AppLayout.tsx" import { Provider } from "@/components/ui/provider"; export function AppLayout({ children }: { children?: React.ReactNode }) { return {children}; } ``` ```tsx title="src/worker.tsx" import { layout, render, route } from "rwsdk/router"; // [!code ++] import { defineApp } from "rwsdk/worker"; import { Document } from "@/app/Document"; import { AppLayout } from "@/app/layouts/AppLayout"; import { setCommonHeaders } from "@/app/headers"; // [!code ++] import { Home } from "@/app/pages/Home"; export default defineApp([ setCommonHeaders(), render(Document, [layout(AppLayout, [route("/", Home)])]), ]); ``` 7. Test Your Installation Try using some Chakra UI components in your app to verify everything is working: ```tsx title="src/app/pages/Home.tsx" import { Button, HStack, Heading } from "@chakra-ui/react"; export function Home() { return (
Welcome to Chakra UI v3
); } ``` 8. Run Development Server
## Customizing Chakra UI [#customizing-chakra-ui] Chakra UI v3 uses a completely new theming system based on the `createSystem` API, inspired by Panda CSS. The old `extendTheme` approach from v2 is no longer used. ### Creating a Custom System [#creating-a-custom-system] Create a theme configuration file: ```ts title="src/theme.ts" import { createSystem, defaultConfig, defineConfig } from "@chakra-ui/react"; const customConfig = defineConfig({ theme: { tokens: { colors: { brand: { 50: { value: "#e6f7ff" }, 100: { value: "#bae7ff" }, 200: { value: "#91d5ff" }, 300: { value: "#69c0ff" }, 400: { value: "#40a9ff" }, 500: { value: "#1890ff" }, 600: { value: "#096dd9" }, 700: { value: "#0050b3" }, 800: { value: "#003a8c" }, 900: { value: "#002766" }, }, }, fonts: { heading: { value: "'Inter', sans-serif" }, body: { value: "'Inter', sans-serif" }, }, }, semanticTokens: { colors: { "bg.primary": { value: { _light: "{colors.white}", _dark: "{colors.gray.900}" }, }, "text.primary": { value: { _light: "{colors.gray.900}", _dark: "{colors.gray.100}" }, }, }, }, }, }); export const system = createSystem(defaultConfig, customConfig); ``` ### Using Your Custom System [#using-your-custom-system] Update the provider to use your custom system: ```tsx title="src/components/ui/provider.tsx" import { ChakraProvider } from "@chakra-ui/react"; import { system } from "@/theme"; // [!code ++] import { ColorModeProvider } from "@/components/ui/color-mode"; export function Provider(props: { children: React.ReactNode }) { return ( // [!code ++:1] {props.children} ); } ``` ### Customization Options [#customization-options] #### Tokens [#tokens] Tokens are the foundation of your design system. They represent raw design values: ```ts defineConfig({ theme: { tokens: { colors: { // Color tokens primary: { value: "#3182ce" }, }, spacing: { // Spacing tokens xs: { value: "0.5rem" }, sm: { value: "1rem" }, }, radii: { // Border radius tokens base: { value: "0.375rem" }, }, }, }, }); ``` #### Semantic Tokens [#semantic-tokens] Semantic tokens provide contextual meaning and can change based on conditions (like color mode): ```ts defineConfig({ theme: { semanticTokens: { colors: { "bg.canvas": { value: { _light: "{colors.white}", _dark: "{colors.gray.950}", }, }, "text.heading": { value: { _light: "{colors.gray.900}", _dark: "{colors.gray.50}", }, }, }, }, }, }); ``` #### Recipes [#recipes] Recipes define component variants and styling patterns: ```ts defineConfig({ theme: { recipes: { button: { base: { fontWeight: "semibold", borderRadius: "md", }, variants: { variant: { solid: { bg: "brand.500", color: "white", }, outline: { borderWidth: "1px", borderColor: "brand.500", color: "brand.500", }, }, }, }, }, }, }); ``` ### Ejecting the Default Theme [#ejecting-the-default-theme] If you want complete control over all tokens and recipes, you can eject the default theme: ```bash npx @chakra-ui/cli eject --outdir src/theme ``` This generates a file containing all default Chakra UI tokens and recipes, which you can then customize as needed. ## Color Mode [#color-mode] Chakra UI v3 uses `next-themes` for color mode management instead of the built-in color mode from v2. ### Using Color Mode [#using-color-mode] The snippet system should have generated a color mode component. You can use the `useColorMode` hook: ```tsx import { Button } from "@chakra-ui/react"; import { useColorMode } from "@/components/ui/color-mode"; export function ColorModeToggle() { const { colorMode, toggleColorMode } = useColorMode(); return ( ); } ``` ### Forcing a Color Mode [#forcing-a-color-mode] To lock a section to a specific color mode, use the `Theme` component: ```tsx import { Theme } from "@chakra-ui/react"; import { ColorModeProvider } from "@/components/ui/color-mode"; export function DarkSection({ children }) { return ( {children} ); } ``` ## Component Changes from v2 to v3 [#component-changes-from-v2-to-v3] ### Common Migration Patterns [#common-migration-patterns] #### Before (v2) [#before-v2] ```tsx Actions Download Create a Copy ``` #### After (v3) [#after-v3] ```tsx Download Create a Copy ``` ### Icons [#icons] The `@chakra-ui/icons` package has been deprecated. Use icon libraries like `react-icons` or `lucide-react`: ```tsx import { Icon } from "@chakra-ui/react"; import { FaDownload } from "react-icons/fa"; export function DownloadButton() { return ( ); } ``` ## Managing Snippets [#managing-snippets] ### Adding More Snippets [#adding-more-snippets] You can add additional snippets at any time: ```bash # Add all snippets npx @chakra-ui/cli snippet add --all # Add a specific snippet npx @chakra-ui/cli snippet add button # List available snippets npx @chakra-ui/cli snippet list # Specify output directory npx @chakra-ui/cli snippet add --outdir ./src/components/ui ``` ### Customizing Snippets [#customizing-snippets] Since snippets are copied directly into your project, you have complete control to modify them. Edit the files in `src/components/ui/` to match your needs. ## Type Generation [#type-generation] Generate TypeScript types for your custom theme to get autocompletion and type safety: ```bash # Generate types for your theme npx @chakra-ui/cli typegen src/theme.ts # Watch for changes and regenerate npx @chakra-ui/cli typegen src/theme.ts --watch # Generate strict types for component variants npx @chakra-ui/cli typegen src/theme.ts --strict ``` ## Further Reading [#further-reading] * [Chakra UI v3 Documentation](https://chakra-ui.com/docs/get-started/installation) * [Migration Guide from v2 to v3](https://chakra-ui.com/docs/get-started/migration) * [Chakra UI CLI Documentation](https://chakra-ui.com/docs/get-started/cli) * [Theming and Customization](https://chakra-ui.com/docs/theming/customization/overview) * [Component Documentation](https://chakra-ui.com/docs/components/concepts/overview) * [next-themes Documentation](https://github.com/pacocoursey/next-themes) --- # Client Side Navigation (Single Page Apps) (https://docs.rwsdk.com/guides/frontend/client-side-nav) ## What is Client Side Navigation? [#what-is-client-side-navigation] Client-side navigation is a technique that allows users to move between pages without a full-page reload. Instead of the browser reloading the entire HTML document, the JavaScript runtime intercepts navigation events (like link clicks), fetches the next page's content (usually as JavaScript modules or RSC payload), and updates the current view. This approach is commonly referred to as a Single Page App (SPA). In RedwoodSDK, you get SPA-like navigation with server-fetched React Server Components (RSC), so it's fast and dynamic, but still uses the server for rendering. RedwoodSDK uses **RSC RPC** to emulate client-side navigation. ```tsx title="src/client.tsx" // [!code word:initClientNavigation] import { initClient, initClientNavigation } from "rwsdk/client"; const { handleResponse, onHydrated } = initClientNavigation(); initClient({ handleResponse, onHydrated }); ``` Once this is initialized, internal `` links will no longer trigger full-page reloads. Instead, the SDK will: 1. Intercept the link click, 2. Push the new URL to the browser's history, 3. Fetch the new page's RSC payload from the server using a **GET** request to the current URL with a `?__rsc` query parameter (making it cache-friendly for browsers and CDNs), 4. And hydrate it on the client. RedwoodSDK keeps everything minimal and transparent. No magic routing system. No nested router contexts. You get the benefits of a modern SPA without giving up control. ## Transitions and View Animations [#transitions-and-view-animations] Client-side navigation enables you to animate between pages without jank. Pair it with View Transitions in React 19 to create seamless visual transitions. ## Caveats [#caveats] No routing system is included: RedwoodSDK doesn't provide a client-side router. You can layer your own state management or page transitions as needed. Only internal links are intercepted: RedwoodSDK will only handle links pointing to the same origin. External links ([https://example.com](https://example.com)) or those with `target="\_blank"` behave normally. Middleware still runs: Every navigation hits your server again — so auth checks, headers, and streaming behavior remain intact. ## Showing Loading UI During Pending Navigation [#showing-loading-ui-during-pending-navigation] Client-side navigation updates the URL before the new RSC tree commits. If a server-backed subtree would otherwise show stale props during that gap, wrap it with `NavigationPending` inside your own Suspense fallback. ```tsx title="src/app/components/PendingResults.tsx" "use client"; import { Suspense } from "react"; import { NavigationPending } from "rwsdk/client"; export function PendingResults({ children }: { children: React.ReactNode }) { return ( }> {children} ); } ``` `` suspends for any pending navigation by default. Pass `searchParams`, `watch`, or `when` when only some URL changes should affect that subtree. Use one option at a time; if you combine them, `when` takes precedence over `watch`, and `watch` takes precedence over `searchParams`. It requires passing the `onHydrated` callback from `initClientNavigation()` into `initClient()`. ### Watch exact URL parts [#watch-exact-url-parts] Use `watch` when a subtree should wait for explicit parts of the URL. In this example, the table only shows its fallback when `search` or `page` changes; a pathname-only or hash-only change will not suspend it. ```tsx }> ``` When using `watch`, `pathname` defaults to `true`, `searchParams` defaults to `true`, and `hash` defaults to `false`. ### Use a custom predicate [#use-a-custom-predicate] Use `when` for app-specific rules. RedwoodSDK passes copies of the current and pending URLs, and the subtree suspends when your predicate returns `true`. ```tsx }> currentUrl.searchParams.get("tab") !== pendingUrl.searchParams.get("tab") } > ``` ## Configuring Scroll Behaviour [#configuring-scroll-behaviour] By default RedwoodSDK jumps to the **top** of the new page the moment the content finishes rendering – just like a traditional full page load. If you would like a different experience you can adjust it with `initClientNavigation`: ```tsx title="Smooth scroll" import { initClientNavigation } from "rwsdk/client"; initClientNavigation({ scrollBehavior: "smooth", }); ``` ### Disable automatic scrolling [#disable-automatic-scrolling] For infinite-scroll feeds or chat applications you might want to *keep* the user exactly where they were, by setting `history.scrollRestoration` to `"manual"` before each navigation action you'll disable the automatic scrolling. ```tsx title="src/client.tsx" history.scrollRestoration = "manual"; // [!code highlight] ``` Alternatively you can set `scrollToTop: false` to disable it completely. ```tsx title="src/client.tsx" initClientNavigation({ scrollToTop: false, // [!code highlight] }); ``` ### Advanced: custom navigation callback [#advanced-custom-navigation-callback] Need to run analytics or state updates before the request is sent? Provide your own `onNavigate` handler: ```tsx initClientNavigation({ scrollBehavior: "auto", onNavigate: async () => { await analytics.track("page_view", { path: window.location.pathname }); }, }); ``` ### Best Practices [#best-practices] * Use the default instant jump for content-heavy pages – it feels identical to a classic navigation and is the least surprising. * Prefer `scrollBehavior: "smooth"` for marketing sites where visual polish is important. * Set `scrollToTop: false` for timelines or lists that the user is expected to scroll through continuously. That’s it! No additional code or router configuration required – RedwoodSDK watches for DOM updates and performs the scroll automatically. ## Programmatic Navigation [#programmatic-navigation] While intercepting link clicks covers most navigation needs, you sometimes need to navigate programmatically - after a form submission, login event, or other user action. The `navigate` function allows you to trigger navigation from anywhere in your code: ```tsx title="Navigate after form submission" import { navigate } from "rwsdk/client"; function handleFormSubmit(event: FormEvent) { event.preventDefault(); navigate("/dashboard"); } ``` ```tsx title="Redirect after login, replacing history" import { navigate } from "rwsdk/client"; async function handleLogin(credentials: Credentials) { await loginUser(credentials); navigate("/account", { history: "replace" }); } ``` ```tsx title="Navigate with custom scroll behavior" import { navigate } from "rwsdk/client"; function handleSpecialAction() { navigate("/results", { info: { scrollBehavior: "smooth", scrollToTop: true, }, }); } ``` The `navigate` function accepts two parameters: * `href`: The destination path * `options`: An optional configuration object with: * `history`: Either `'push'` (default) to add a new history entry, or `'replace'` to replace the current one * `info.scrollToTop`: Whether to scroll to the top after navigation (default: `true`) * `info.scrollBehavior`: How to scroll - `'instant'` (default), `'smooth'`, or `'auto'` ## Prefetching Routes [#prefetching-routes] You can improve navigation performance by prefetching routes that users are likely to visit next. RedwoodSDK automatically detects `` elements in your pages and fetches those routes in the background. ### How it Works [#how-it-works] After each client-side navigation, RedwoodSDK scans the document for `` elements that point to same-origin routes. For each x-prefetch link found, it issues a background GET request with the `__rsc` query parameter and an `x-prefetch: true` header. Successful responses are stored in the browser's Cache API. When a user navigates to a prefetched route, the cached response is used instead of making a network request, resulting in instant navigation. ### Basic Usage [#basic-usage] Add `` tags to your pages or layouts to hint at likely next destinations: ```tsx title="In a route or layout component (React 19)" import { link } from "@/shared/links"; export function HomePage() { const aboutHref = link("/about"); const contactHref = link("/contact"); return ( <> {/* React 19 will hoist these tags into */}

Welcome

); } ``` ### Prefetching from Navigation Links [#prefetching-from-navigation-links] A common pattern is to prefetch routes that are linked from the current page: ```tsx title="Prefetching linked routes" import { link } from "@/shared/links"; export function BlogListPage({ posts }) { return ( <> {posts.map((post) => { const postHref = link("/blog/:slug", { slug: post.slug }); return ( ); })} ); } ``` ### Cache Management [#cache-management] RedwoodSDK uses a generation-based cache eviction pattern: * Cache entries are automatically cleaned up after each navigation to ensure fresh content * Each browser tab maintains its own cache namespace * The system avoids races with in-flight prefetch requests * Cache entries are stored using the browser's Cache API, following standard web platform semantics This ensures that prefetched content stays fresh while providing the performance benefits of cached navigation. ## API Reference [#api-reference] ### `initClientNavigation(options?)` [#initclientnavigationoptions-] Initializes the client-side navigation. Call this function from your `client.tsx` entry point. **Returns:** An object with two properties that should be passed to `initClient`: * `handleResponse`: A function that handles navigation responses and errors (required for error handling) * `onHydrated`: A function that runs after each hydration to manage cache eviction and prefetching (optional but recommended; required if using prefetching) **Parameters:** * `options` (optional): A `ClientNavigationOptions` object: * `scrollToTop` (boolean, default: `true`): Whether to scroll to the top after navigation * `scrollBehavior` (`'instant' | 'smooth' | 'auto'`, default: `'instant'`): How scrolling happens * `onNavigate` (function, optional): Callback executed after history push but before RSC fetch **Example:** ```tsx title="src/client.tsx" import { initClient, initClientNavigation } from "rwsdk/client"; const { handleResponse, onHydrated } = initClientNavigation(); initClient({ handleResponse, onHydrated }); ``` **Example with options:** ```tsx title="src/client.tsx" import { initClient, initClientNavigation } from "rwsdk/client"; const { handleResponse, onHydrated } = initClientNavigation({ scrollBehavior: "smooth", scrollToTop: true, }); initClient({ handleResponse, onHydrated }); ``` --- # Dark / Light Mode (https://docs.rwsdk.com/guides/frontend/dark-mode) This guide demonstrates how to implement dark and light mode themes in your RedwoodSDK application. The approach uses cookies to persist user preferences and direct DOM manipulation to toggle themes, without requiring a React context provider. ## Overview [#overview] The theme system supports three modes: * **`dark`**: Always use dark mode * **`light`**: Always use light mode * **`system`**: Follow the user's system preference The implementation follows this flow: ``` worker (read theme from cookie) ⮑ Document (set class on , calculate system theme before render) ⮑ Page components ⮑ ThemeToggle (client component that updates DOM and cookie) ``` ## Implementation [#implementation] 1. **Read theme from cookie in the worker** In your `src/worker.tsx`, read the theme cookie and add it to the app context: ```tsx title="src/worker.tsx" import { render, route } from "rwsdk/router"; import { defineApp } from "rwsdk/worker"; import { Document } from "@/app/Document"; import { Home } from "@/app/pages/Home"; export interface AppContext { theme?: "dark" | "light" | "system"; } export default defineApp([ ({ ctx, request }) => { // Read theme from cookie const cookie = request.headers.get("Cookie"); const match = cookie?.match(/theme=([^;]+)/); ctx.theme = (match?.[1] as "dark" | "light" | "system") || "system"; }, render(Document, [route("/", Home)]), ]); ``` 2. **Create a server action to set the theme** Create a server function that updates the theme cookie: ```tsx title="src/app/actions/setTheme.ts" "use server"; import { requestInfo } from "rwsdk/worker"; export async function setTheme(theme: "dark" | "light" | "system") { requestInfo.response.headers.set( "Set-Cookie", `theme=${theme}; Path=/; Max-Age=31536000; SameSite=Lax`, ); } ``` 3. **Update Document to set theme class before render** The `Document` component needs to set the theme class on the `` element before React hydrates to prevent FOUC. This requires a small inline script: ```tsx title="src/app/Document.tsx" import React from "react"; import { requestInfo } from "rwsdk/worker"; import stylesUrl from "./styles.css?url"; export const Document: React.FC<{ children: React.ReactNode }> = ({ children, }) => { const theme = requestInfo?.ctx?.theme || "system"; return ( My App {/* Script to set theme class before React hydrates */} ); }; ``` 4. **Create a theme toggle component** Create a client component that toggles the theme by directly manipulating the DOM and calling the server action: ```tsx title="src/app/components/ThemeToggle.tsx" "use client"; import { useEffect, useRef, useState } from "react"; import { setTheme } from "../actions/setTheme"; type Theme = "dark" | "light" | "system"; export function ThemeToggle({ initialTheme }: { initialTheme: Theme }) { const [theme, setThemeState] = useState(initialTheme); const isInitialMount = useRef(true); // Update DOM when theme changes useEffect(() => { const root = document.documentElement; const shouldBeDark = theme === "dark" || (theme === "system" && window.matchMedia("(prefers-color-scheme: dark)").matches); if (shouldBeDark) { root.classList.add("dark"); } else { root.classList.remove("dark"); } // Set data attribute for consistency root.setAttribute("data-theme", theme); // Persist to cookie via server action (only when theme actually changes, not on initial mount) if (!isInitialMount.current) { setTheme(theme).catch((error) => { console.error("Failed to set theme:", error); }); } else { isInitialMount.current = false; } }, [theme]); // Listen for system theme changes when theme is "system" useEffect(() => { if (theme !== "system") return; const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); const handleChange = () => { const root = document.documentElement; if (mediaQuery.matches) { root.classList.add("dark"); } else { root.classList.remove("dark"); } }; mediaQuery.addEventListener("change", handleChange); return () => mediaQuery.removeEventListener("change", handleChange); }, [theme]); const toggleTheme = () => { // Cycle through: system -> light -> dark -> system if (theme === "system") { setThemeState("light"); } else if (theme === "light") { setThemeState("dark"); } else { setThemeState("system"); } }; return (
Current theme: {theme}
); } ``` 5. **Use the theme toggle in your pages** Pass the theme from the context to your toggle component: ```tsx title="src/app/pages/Home.tsx" import { RequestInfo } from "rwsdk/worker"; import { ThemeToggle } from "../components/ThemeToggle"; export function Home({ ctx }: RequestInfo) { const theme = ctx.theme || "system"; return (

Welcome

); } ```
## Reading the Current Theme [#reading-the-current-theme] The `ThemeToggle` component above includes a display of the current theme. If you need to read the current theme in a separate client component, you can check the DOM directly: ```tsx title="src/app/components/MyComponent.tsx" "use client"; import { useEffect, useState } from "react"; export function MyComponent() { const [isDark, setIsDark] = useState(false); useEffect(() => { // Check if dark class is present setIsDark(document.documentElement.classList.contains("dark")); // Optional: Listen for changes const observer = new MutationObserver(() => { setIsDark(document.documentElement.classList.contains("dark")); }); observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class"], }); return () => observer.disconnect(); }, []); return (

Current theme: {isDark ? "dark" : "light"}

); } ``` ## CSS Styling [#css-styling] With Tailwind CSS, you can use the `dark:` variant to style elements differently in dark mode: ```css title="src/app/styles.css" @import "tailwindcss"; @custom-variant dark (&:is(.dark *)); /* Your custom styles */ .my-component { background-color: white; color: black; } .dark .my-component { background-color: #1a1a1a; color: white; } ``` Or with Tailwind utility classes: ```tsx
Content
``` ## Alternative: Data Attributes [#alternative-data-attributes] If you prefer using data attributes instead of class names, you can modify the `Document` and toggle component: ```tsx title="src/app/Document.tsx" // In the script document.documentElement.setAttribute("data-theme", theme); ``` ```tsx title="src/app/components/ThemeToggle.tsx" // In the useEffect root.setAttribute("data-theme", theme); ``` Then in your CSS: ```css [data-theme="dark"] .my-component { background-color: #1a1a1a; } ``` ## Further Reading [#further-reading] * [Dark Mode Playground](https://github.com/redwoodjs/sdk/tree/main/playground/dark-mode) - Complete working example * [Tailwind CSS Dark Mode](https://tailwindcss.com/docs/dark-mode) --- # Documents (https://docs.rwsdk.com/guides/frontend/documents) In RedwoodSDK, Document components give you complete control over the HTML structure of each route. Unlike many frameworks that use a fixed HTML document structure, RedwoodSDK lets you define custom documents per route, controlling everything from the doctype to scripts and hydration strategy. ## A Basic Document [#a-basic-document] 1. The starter project comes with a Document component. ```tsx title="src/app/Document.tsx" export const Document: React.FC<{ children: React.ReactNode }> = ({ children, }) => ( RedwoodSDK App {children} ); ``` 2. Use the Document component in your routes: ```tsx title="src/worker.tsx" // [!code word:Document] import { defineApp } from 'rwsdk/worker' import { render, route } from 'rwsdk/router' import { Document } from '@/app/Document.tsx' // [!code highlight] import { HomePage } from '@/app/pages/HomePage.tsx' export default defineApp([ render(Document, [ // [!code highlight] route('/', HomePage), ]) ]) ``` ## Multiple Document Types [#multiple-document-types] One of the most powerful features of RedwoodSDK is the ability to use different Document components for different routes. You can create a specialized Document component for a static document, a realtime document, or an application document. Then, use different Documents for different routes: ```tsx title="src/worker.tsx" collapse={8-11} // [!code word:StaticDocument] // [!code word:ApplicationDocument] // [!code word:RealtimeDocument] import { defineApp } from 'rwsdk/worker' import { render, route, prefix } from 'rwsdk/router' import { StaticDocument } from '@/app/StaticDocument.tsx' import { ApplicationDocument } from '@/app/ApplicationDocument.tsx' import { RealtimeDocument } from '@/app/RealtimeDocument.tsx' import { HomePage } from '@/app/pages/HomePage.tsx' import { blogRoutes } from '@/app/routes/blog.tsx' import { userRoutes } from '@/app/routes/user.tsx' import { dashboardRoutes } from '@/app/routes/dashboard.tsx' export default defineApp([ render(StaticDocument, [ route('/', HomePage), prefix('/blog', blogRoutes), ]), render(ApplicationDocument, [ prefix('/app/user', userRoutes), ]), render(RealtimeDocument, [ prefix('/app/dashboard', dashboardRoutes), ]) ]) ``` ## Further Reading [#further-reading] * [Blog Post: Per-Route Documents in RedwoodSDK: Total Control Over Your HTML](https://rwsdk.com/blog/redwoodsdk-multiple-documents) --- # Error Handling (https://docs.rwsdk.com/guides/frontend/error-handling) RedwoodSDK supports React 19's powerful error handling APIs, allowing you to catch and handle errors at the React root level. This enables production-ready error monitoring, custom recovery strategies, and better debugging capabilities. ## Overview [#overview] React 19 introduced two main error handling APIs: * **`onUncaughtError`**: Handles uncaught errors that escape error boundaries (async errors, event handler errors, etc.) * **`onCaughtError`**: Handles errors that are caught by error boundaries These APIs are available through the `hydrateRootOptions` parameter in `initClient`, which passes options directly to React's `hydrateRoot` function. ## When to Use Each Handler [#when-to-use-each-handler] ### `onUncaughtError` [#onuncaughterror] Use `onUncaughtError` for errors that occur during the React lifecycle but are not caught by error boundaries: * Errors during initial hydration or rendering. * Errors inside `useEffect` or other lifecycle hooks. * Errors during React transitions. :::caution Errors in imperative event handlers (e.g., `onClick`) or asynchronous timers (e.g., `setTimeout`) often bubble directly to the browser and may not be caught by `onUncaughtError`. For these, you should use global browser handlers (see [Universal Error Handling](#universal-error-handling) below). ::: ```tsx title="Example: Uncaught error in lifecycle" "use client"; import { useEffect } from "react"; export function Component() { useEffect(() => { // This error will trigger onUncaughtError throw new Error("Lifecycle error"); }, []); return
Component
; } ``` ### `onCaughtError` [#oncaughterror] Use `onCaughtError` for errors that are caught by error boundaries: * Component rendering errors * Errors in component lifecycle methods * Errors caught by `` components ```tsx title="Example: Error caught by error boundary" "use client"; export function ErrorBoundary({ children }: { children: React.ReactNode }) { // This error will trigger onCaughtError return {children}; } export function Component() { throw new Error("Component error"); return
This won't render
; } ``` ## Basic Setup [#basic-setup] 1. Import `initClient` from `rwsdk/client`: ```tsx title="src/client.tsx" import { initClient } from "rwsdk/client"; ``` 2. Configure error handlers via `hydrateRootOptions`: ```tsx title="src/client.tsx" initClient({ hydrateRootOptions: { onUncaughtError: (error, errorInfo) => { console.error("Uncaught error:", error); console.error("Component stack:", errorInfo.componentStack); }, onCaughtError: (error, errorInfo) => { console.error("Caught error:", error); console.error("Component stack:", errorInfo.componentStack); }, }, }); ``` 3. The error handlers will now catch and log all React errors in your application. ## Universal Error Handling [#universal-error-handling] To ensure that *all* client-side errors (including event handlers, timeouts, and promise rejections) are caught and handled uniformly, you should combine React's error handlers with global browser listeners. This pattern is particularly useful for redirecting users to a dedicated error page on any fatal error: ```tsx title="src/client.tsx" import { initClient } from "rwsdk/client"; const redirectToError = () => { // Use replace to avoid keeping the broken page in history window.location.replace("/error"); }; // 1. Catch imperative errors (event handlers, timeouts, etc.) window.addEventListener("error", (event) => { console.error("Global error caught:", event.message); redirectToError(); }); // 2. Catch unhandled promise rejections window.addEventListener("unhandledrejection", (event) => { console.error("Unhandled promise rejection:", event.reason); redirectToError(); }); initClient({ hydrateRootOptions: { // 3. Catch React-specific uncaught errors (rendering, hydration) onUncaughtError: (error, errorInfo) => { console.error("React uncaught error:", error, errorInfo); redirectToError(); }, // 4. Catch errors caught by error boundaries onCaughtError: (error, errorInfo) => { console.error("React caught error:", error, errorInfo); redirectToError(); }, }, }); ``` ## Integration with Monitoring Services [#integration-with-monitoring-services] ### Sentry [#sentry] ```tsx title="src/client.tsx" import { initClient } from "rwsdk/client"; import * as Sentry from "@sentry/browser"; initClient({ hydrateRootOptions: { onUncaughtError: (error, errorInfo) => { Sentry.captureException(error, { contexts: { react: { componentStack: errorInfo.componentStack, errorBoundary: errorInfo.errorBoundary?.constructor.name, }, }, tags: { errorType: "uncaught" }, }); }, onCaughtError: (error, errorInfo) => { Sentry.captureException(error, { contexts: { react: { componentStack: errorInfo.componentStack, errorBoundary: errorInfo.errorBoundary?.constructor.name, }, }, tags: { errorType: "caught" }, }); }, }, }); ``` ### Custom Monitoring Service [#custom-monitoring-service] ```tsx title="src/client.tsx" import { initClient } from "rwsdk/client"; function sendToMonitoring( error: unknown, errorInfo: { componentStack: string; errorBoundary?: React.Component | null }, type: "uncaught" | "caught", ) { fetch("/api/errors", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, componentStack: errorInfo.componentStack, errorBoundary: errorInfo.errorBoundary?.constructor.name, type, timestamp: new Date().toISOString(), }), }); } initClient({ hydrateRootOptions: { onUncaughtError: (error, errorInfo) => { sendToMonitoring(error, errorInfo, "uncaught"); }, onCaughtError: (error, errorInfo) => { sendToMonitoring(error, errorInfo, "caught"); }, }, }); ``` ## Error Recovery Strategies [#error-recovery-strategies] ### Show User-Friendly Messages [#show-user-friendly-messages] ```tsx title="src/client.tsx" import { initClient } from "rwsdk/client"; function showErrorToast(message: string) { // Your toast implementation console.log("Error:", message); } initClient({ hydrateRootOptions: { onUncaughtError: (error, errorInfo) => { // Log for debugging console.error("Uncaught error:", error, errorInfo); // Show user-friendly message showErrorToast("Something went wrong. Please try again."); // Send to monitoring sendToMonitoring(error, errorInfo); }, }, }); ``` ### Reload on Critical Errors [#reload-on-critical-errors] ```tsx title="src/client.tsx" import { initClient } from "rwsdk/client"; function isCriticalError(error: unknown): boolean { // Define your critical error logic return error instanceof Error && error.message.includes("CRITICAL"); } initClient({ hydrateRootOptions: { onUncaughtError: (error, errorInfo) => { console.error("Uncaught error:", error, errorInfo); if (isCriticalError(error)) { // Reload page for critical errors window.location.reload(); } else { // Handle non-critical errors gracefully showErrorToast("An error occurred. Please refresh the page."); } }, }, }); ``` ## Best Practices [#best-practices] ### 1. Always Log Errors [#1-always-log-errors] Even if you're sending errors to a monitoring service, log them locally for debugging: ```tsx onUncaughtError: (error, errorInfo) => { console.error("Uncaught error:", error); console.error("Component stack:", errorInfo.componentStack); // Then send to monitoring }; ``` ### 2. Include Component Stack [#2-include-component-stack] The `errorInfo.componentStack` provides valuable debugging information. Always include it in your error reports: ```tsx Sentry.captureException(error, { contexts: { react: { componentStack: errorInfo.componentStack, }, }, }); ``` ### 3. Distinguish Error Types [#3-distinguish-error-types] Use tags or metadata to distinguish between caught and uncaught errors: ```tsx onUncaughtError: (error, errorInfo) => { sendToMonitoring(error, { ...errorInfo, type: "uncaught" }); }, onCaughtError: (error, errorInfo) => { sendToMonitoring(error, { ...errorInfo, type: "caught" }); }, ``` ### 4. Don't Block the UI [#4-dont-block-the-ui] Error handlers should not throw errors themselves. Keep them lightweight: ```tsx onUncaughtError: (error, errorInfo) => { try { // Safe error handling sendToMonitoring(error, errorInfo); } catch (e) { // Fallback to console if monitoring fails console.error("Error in error handler:", e); } }, ``` ## Server-Side Error Handling [#server-side-error-handling] For server-side errors (errors in Server Components, middleware, route handlers, and RSC actions), use the `except` function from `rwsdk/router`. This provides a declarative way to handle errors that integrates with your routing structure. ### Basic Usage [#basic-usage] ```tsx title="src/worker.tsx" import { except, route } from "rwsdk/router"; import { defineApp } from "rwsdk/worker"; export default defineApp([ except((error) => { console.error("Server error:", error); return ; }), route("/", () => ), ]); ``` ### Integration with Monitoring [#integration-with-monitoring] You can combine `except` with monitoring services. Since monitoring calls are often asynchronous, use `ctx.waitUntil()` to ensure the worker doesn't terminate before the error is sent: ```tsx title="src/worker.tsx" import { except, route } from "rwsdk/router"; import { defineApp } from "rwsdk/worker"; export default defineApp([ except(async (error, { request, cf: ctx }) => { // Send to monitoring service asynchronously without blocking the response ctx.waitUntil( sendToMonitoring(error, { url: request.url, method: request.method, }), ); // Return user-friendly error page return ; }), route("/", () => ), ]); ``` ### Nested Error Handling [#nested-error-handling] You can define multiple `except` handlers for different sections of your application: ```tsx title="src/worker.tsx" import { except, prefix, route } from "rwsdk/router"; import { defineApp } from "rwsdk/worker"; export default defineApp([ // Global error handler except((error) => { return ; }), prefix("/api", [ // API-specific error handler except((error) => { return Response.json( { error: error instanceof Error ? error.message : "API Error" }, { status: 500 }, ); }), route("/users", async () => { // This error will be caught by the API handler throw new Error("Database error"); }), ]), route("/", () => ), ]); ``` For more details on `except`, see the [router documentation](/reference/sdk-router/#except). ## Relationship to Error Boundaries [#relationship-to-error-boundaries] Error boundaries are React components that catch errors in their child component tree. However, they have important limitations in a React Server Components (RSC) world: * **Client-only**: Error boundaries only work in client components (`"use client"`), not in server components * **Forces client components**: Using error boundaries requires nesting components inside them, which forces all child components to be client components, defeating the purpose of RSC * **Limited placement**: In RSC architectures, there's often no good place to wrap server components with error boundaries since they render on the server * **Post-hydration only**: Error boundaries only catch errors after client-side hydration, not during initial server rendering Root-level error handlers (`onUncaughtError` and `onCaughtError`) are more suitable for RSC applications because they: * Work for both server-rendered and client-rendered errors (post-hydration) * Don't require wrapping components or converting them to client components * Catch errors that escape error boundaries * Provide a single place to handle all React errors for monitoring and logging * Preserve the benefits of RSC by not forcing components to be client components For server-side rendering errors, use the `except` function from `rwsdk/router` (see [router error handling documentation](/reference/sdk-router/#except)). ## Scope and Limitations [#scope-and-limitations] ### What These APIs Handle [#what-these-apis-handle] * Component rendering errors (post-hydration). * Errors inside `useEffect` or other lifecycle methods. * Errors during React transitions. * Errors that escape error boundaries. ### What They Don't Handle Reliably [#what-they-dont-handle-reliably] * **Imperative event handlers**: Errors in `onClick`, `onBlur`, etc., often bubble directly to the browser. * **Asynchronous code**: `setTimeout`, `setInterval`, or third-party callbacks outside of React's control. * **Unhandled rejections**: Promise failures that are not part of a React transition. * **Server-side RSC rendering errors**: Use the [`except` function](/reference/sdk-router/#except) or wrap `defineApp`'s `fetch` method. * **SSR errors**: Handled server-side. ## Common Patterns [#common-patterns] ### Pattern 1: Development vs Production [#pattern-1-development-vs-production] ```tsx title="src/client.tsx" import { initClient } from "rwsdk/client"; const isDevelopment = import.meta.env.DEV; initClient({ hydrateRootOptions: { onUncaughtError: (error, errorInfo) => { if (isDevelopment) { // Detailed logging in development console.error("Uncaught error:", error); console.error("Component stack:", errorInfo.componentStack); } else { // Send to monitoring in production sendToMonitoring(error, errorInfo); } }, }, }); ``` ### Pattern 2: User Feedback [#pattern-2-user-feedback] ```tsx title="src/client.tsx" import { initClient } from "rwsdk/client"; initClient({ hydrateRootOptions: { onUncaughtError: (error, errorInfo) => { // Log error console.error("Uncaught error:", error, errorInfo); // Send to monitoring sendToMonitoring(error, errorInfo); // Show user feedback const errorMessage = error instanceof Error ? error.message : "Unknown error"; showErrorNotification(`Error: ${errorMessage}`); }, }, }); ``` ## Summary [#summary] React 19's error handling APIs provide powerful tools for monitoring and handling errors in production. By configuring `onUncaughtError` and `onCaughtError` through `hydrateRootOptions`, you can: * Track errors in production * Integrate with monitoring services * Implement custom recovery strategies * Improve debugging with component stacks These root-level error handlers are particularly well-suited for React Server Components applications, where traditional error boundaries have limited utility. --- # Layouts (https://docs.rwsdk.com/guides/frontend/layouts) RedwoodSDK provides a powerful `layout()` function for creating shared UI layouts across your routes. This allows you to maintain consistent page structures, implement nested layouts, and avoid code duplication. ### Key Features [#key-features] * Composable: Works seamlessly with existing `prefix()`, `render()`, and `route()` functions * Nested Support: Multiple `layout()` calls create properly nested component hierarchies * SSR/RSC Safe: Automatic client component detection prevents serialization errors * Middleware Friendly: Preserves middleware functions in route arrays ## Example with Code [#example-with-code] 1. Create a layout component: ```tsx title="src/app/layouts/AppLayout.tsx" import type { LayoutProps } from 'rwsdk/router' export function AppLayout({ children, requestInfo }: LayoutProps) { return (
{requestInfo && ( Path: {new URL(requestInfo.request.url).pathname} )}
{children}
© {new Date().getFullYear()}
); } ``` 2. Use the layout in your routes: ```tsx title="src/app/worker.tsx" import { layout, route, render } from 'rwsdk/router' import { AppLayout } from './layouts/AppLayout' import HomePage from './pages/HomePage' import AboutPage from './pages/AboutPage' export default defineApp([ render(Document, [ layout(AppLayout, [ route("/", HomePage), route("/about", AboutPage), ]) ]) ]) ``` 3. Create nested layouts: ```tsx title="src/app/layouts/AdminLayout.tsx" import type { LayoutProps } from 'rwsdk/router' export function AdminLayout({ children }: LayoutProps) { "use client" // Client component example return (
{children}
); } ``` 4. Combine layouts with other router functions: ```tsx title="src/app/worker.tsx" export default defineApp([ render(Document, [ layout(AppLayout, [ route("/", HomePage), prefix("/admin", [ layout(AdminLayout, [ route("/", AdminDashboard), route("/users", UserManagement), ]) ]) ]) ]) ]) ```
## Layout Props [#layout-props] Layout components receive two props: * `children`: The wrapped route content * `requestInfo`: Request context (only passed to server components) There's a specific type for the `LayoutProps` prop: ```tsx title="src/app/layouts/AppLayout.tsx" import type { LayoutProps } from 'rwsdk/router' export function AppLayout({ children, requestInfo }: LayoutProps) { ... ``` ## Complex Composition [#complex-composition] Each of these examples work: ```tsx title="src/app/worker.tsx" prefix("/api", layout(ApiLayout, routes)) // ✅ layout(AppLayout, prefix("/admin", routes)) // ✅ render(Document, layout(AppLayout, routes)) // ✅ `` ``` --- # Meta Data (https://docs.rwsdk.com/guides/frontend/metadata) [React 19](https://react.dev/blog/2024/12/05/react-19#support-for-metadata-tags) introduced a more streamlined approach to managing document metadata. In RedwoodSDK, you can leverage these conventions to easily add and manage meta tags for SEO, social sharing, and other purposes directly within your components. ## Title and Meta Tags [#title-and-meta-tags] Meta tags are directly built-in to React 19, with `` component: ```tsx {6-8} import React from "react"; export default function ProductPage() { return ( <> Product Name

Product Name

{/* Rest of your component */} ); } ``` When this component renders, React will automatically handle updating the document's `` section. ## Complete SEO Setup [#complete-seo-setup] Here's a more comprehensive example including Open Graph and Twitter card meta tags: ```tsx import React from "react"; export default function BlogPostPage({ post }) { const { title, description, image, publishDate, author } = post; return ( <> {/* Basic Meta Tags */} {title} | My Blog {/* Open Graph / Facebook */} {/* Twitter */} {/* Canonical URL */} {/* Page Content */}

{title}

{/* Rest of your blog post content */}
); } ``` ## Further Reading [#further-reading] * [React 19 Documentation](https://react.dev/) * [Google SEO Documentation](https://developers.google.com/search/docs/fundamentals/seo-starter-guide) * [Open Graph Protocol](https://ogp.me/) * [Twitter Cards Documentation](https://developer.twitter.com/en/docs/twitter-for-websites/cards/overview/abouts-cards) * [Schema.org](https://schema.org/) for structured data --- # Open Graph Images (https://docs.rwsdk.com/guides/frontend/og-images) An Open Graph (OG) image is a specific image used when a webpage is shared on social media platforms like Facebook, LinkedIn, and Twitter/X. It serves as a visual preview that appears in link shares, providing a visual representation of the page's content. These are defined through the page's meta tags: ```html ``` You can create a default, static OG image for the entire project, however, custom OG images are recommended for a better social sharing experience. Within React 19, you can include `meta` tags directly within your page components and they'll be rendered in the head. ([More details on the Meta Data documentation](/guides/frontend/metadata/)) *** ## Creating Dynamic OG Images [#creating-dynamic-og-images] There's a fantastic package called `workers-og` that allows you to create dynamic Open Graph images using Cloudflare Workers. 🙌 First, install the `workers-og` package: ```bash pnpm install workers-og ``` Now, we have two options. You can use standard HTML and CSS to create your Open Graph image, or you can use a React component. ## Using HTML and CSS [#using-html-and-css] Within the `worker.tsx` file, let's create a new route, called `/og`: ```tsx title="src/worker.tsx" {"1. Defined a Title": 3} {"2. Set up the HTML": 5} {"3. Return the Image": 14} render(Document, [ route("/og", () => { const title = "Hello, World!"; const html = `

${title}

`; return new ImageResponse(html, { width: 1200, height: 630, }); }), ``` In this example, I hard coded the title, `Hello, World!`. However, you can pass parameters through the URL and make database calls to fetch the data you need. Then, when returning the image, you'll notice I'm passing in the `html` variable and specifying the `width` and `height` of the image. Within the browser, you can visit the `/og` route to see the image: ## Using React [#using-react] You can also use a React component, which probably feels more natural, especially for passing around props and parameters. For this example, I'm going to create a new component inside the `src/app/components` directory, called `Og.tsx`: ```tsx title="src/app/components/Og.tsx" const Og = ({ title }: { title: string }) => { return (

{title}

) } export default Og ``` Now, within your `worker.tsx` file, let's create a new route, called `/og-react`: ```tsx title="src/worker.tsx" {"1. Import the Og Component": 1} {"2. Pass in the Title": 5} {"3. Return the Image": 9} . import Og from "@/app/components/Og"; ... route("/og-react", () => { const title = "Hello, Amy!"; const og = ; return new ImageResponse(og, { width: 1200, height: 630, }); }), ``` Within the browser, you can visit the `/og-react` route to see the image: ## Updating the Meta Tags [#updating-the-meta-tags] Now that you have your dynamic OG image, you can update the meta tags in your page component to use the new OG image. ```tsx title="src/app/pages/Home.tsx" ``` You can test your OG image by visiting the [Open Graph Image Tester](https://www.opengraph.xyz/) and entering your URL (not localhost). ## Further Reading [#further-reading] * [Example Repo](https://github.com/ahaywood/og-kitchen) * [workers-og](https://github.com/kvnang/workers-og/tree/main) * [Open Graph Image Tester](https://www.opengraph.xyz/) --- # Public Assets (https://docs.rwsdk.com/guides/frontend/public-assets) ## Setting Up the Public Directory [#setting-up-the-public-directory] RedwoodSDK provides a simple way to serve static assets like images, fonts, and other files through the public directory. 1. Create a `public` directory in the root of your project: ```bash mkdir public ``` 2. Place any static assets you want to serve in this directory: * public/ * images/ * logo.png * background.jpg * fonts/ * custom-font.woff2 * documents/ * sample.pdf * favicon.ico 3. Access your static assets in your application using root-relative URLs: ```tsx // In your component function Header() { return (
Logo

My Application

); } ``` Or, for custom fonts, reference them in your CSS: ```css @font-face { font-family: "CustomFont"; src: url("/fonts/custom-font.woff2") format("woff2"); } ```
## Common Use Cases [#common-use-cases] ### Images and Media [#images-and-media] Store and serve images, videos, and other media files: ```tsx Hero Banner ``` ### Fonts [#fonts] Host custom font files for your application: ```css /* In your CSS */ @font-face { font-family: "BrandFont"; src: url("/fonts/brand-font.woff2") format("woff2"); font-weight: 400; font-style: normal; } /* Then use it with Tailwind */ @theme { --font-brand: "BrandFont", sans-serif; } ``` ### Favicon and Browser Icons [#favicon-and-browser-icons] Store favicon and other browser icons: ```tsx // In your Document.tsx ``` ## Production Considerations [#production-considerations] In production, files in the public directory: * Do not go through the JavaScript bundling process * Maintain their file structure and naming ## Further Reading [#further-reading] * [Static File Serving in Vite](https://vitejs.dev/guide/assets.html#the-public-directory) * [Image Optimization Best Practices](https://web.dev/fast/#optimize-your-images) * [Web Font Best Practices](https://web.dev/font-best-practices/) --- # shadcn/ui (https://docs.rwsdk.com/guides/frontend/shadcn) ## Installing shadcn/ui [#installing-shadcnui] 1. [Install TailwindCSS](/guides/frontend/tailwind). 2. Install shadcn/ui It will ask you what theme you want to use. This command will create a `components.json` file in the root of your project. It contains all the configuration for our shadcn/ui components. If you want to match RedwoodSDK conventions, add the following aliases to the `components.json` file: ```json title="components.json" lineNumbers=13 ... "aliases": { "components": "@/app/components", "utils": "@/app/lib/utils", "ui": "@/app/components/ui", "lib": "@/app/lib", "hooks": "@/app/hooks" }, ... ``` 3. Now, you should be able to add components: You can add components in bulk by running: Or, you can add a single component by running: Components will be added to the `src/app/components/ui` folder. * src/ * app/ * components/ * ui/ ## Toaster (sonner) [#toaster-sonner] By default, the shadcn `Toaster` (from `sonner`) might not work if added directly to the `Document` because it needs to be client-side and properly encapsulated within the route tree where toasts are triggered. To make it work: 1. Create a "Client Component" for the Toaster. 2. Create a Layout that encapsulates your routes. 3. Render the Toaster within that Layout. ```tsx title="src/app/components/toaster.tsx" "use client"; import { Toaster as Sonner } from "@/app/components/ui/sonner"; export function Toaster() { return ; } ``` ```tsx title="src/app/layouts/main-layout.tsx" import { Toaster } from "@/app/components/Toaster"; export function MainLayout({ children }: { children: React.ReactNode }) { return ( <> {children} ); } ``` ## Further reading [#further-reading] * [ShadCN](https://ui.shadcn.com/) * [TailwindCSS v4](https://tailwindcss.com/docs/installation/using-vite) --- # Storybook (https://docs.rwsdk.com/guides/frontend/storybook) ## Installing Storybook [#installing-storybook] Because the RedwoodSDK is based on React and Vite, we can work through the "React & Vite" documentation: 1. Install Storybook: 2. Select what we want to use Storybook for — I selected both Documentation and Testing, though this guide will only cover the documentation part: 3. It'll say it can't detect the framework. Select React — it'll automatically detect Vite: 4. Storybook will finish installing, and then start our Storybook server: 5. It should automatically open our browser to Storybook, and if it doesn't, we can go to `localhost:6006` to see it: 6. It also added `storybook` and `storybook-build` scripts to our `package.json` file. We can always run the `storybook` script to start the Storybook server, and `storybook-build` script to build our Storybook site for production: ```json { "scripts": { "storybook": "storybook dev -p 6006", "storybook-build": "storybook build" } } ``` ## Adding a Component to Storybook [#adding-a-component-to-storybook] In writing this guide, we've started by following the [quick start instructions](../../getting-started/quick-start) and set up the starter project. The starter project comes with a very basic `Home` component: ```tsx title="src/app/pages/Home.tsx" import { RequestInfo } from "rwsdk/worker"; export function Home({ ctx }: RequestInfo) { return (

{ctx.user?.username ? `You are logged in as user ${ctx.user.username}` : "You are not logged in"}

); } ``` Given that this is very basic, we'd most likely want to build this out a bit more. Storybook is the perfect place to do that! Let's see what that looks like. 1. Create a new file: `src/app/pages/Home.stories.tsx` ```tsx title="src/app/pages/Home.stories.tsx" import type { Meta, StoryObj } from "@storybook/react"; import { Home } from "./Home"; const meta: Meta = { component: Home, }; export default meta; type Story = StoryObj; export const NotLoggedIn: Story = { args: { ctx: { user: null, session: null, }, }, }; ``` 2. Save, and go back to our Storybook site. We should see a new "Home" section in the sidebar: 3. Great! What if we want to mock the logged in user? We can do that by adding a new story, this time passing in a user object to the `ctx` prop: ```tsx title="src/app/pages/Home.stories.tsx" import type { Meta, StoryObj } from "@storybook/react"; import { Home } from "./Home"; const meta: Meta = { component: Home, }; export default meta; type Story = StoryObj; export const NotLoggedIn: Story = { args: { ctx: { user: null, session: null, }, }, }; // [!code ++:12] export const LoggedIn: Story = { args: { ctx: { user: { id: "1", username: "redwood_fan_123", createdAt: new Date(), }, session: null, }, }, }; ``` 4. Save it, and go back to our Storybook site. We should see a new "Logged In" story: 5. Great! But what if we want to be able to play around with the username that's displayed? Sure, we can always click into the generated controls and change the username, but it's a little ugly. What if we want to just have a dropdown with some options?

Thankfully, Storybook lets us override the generated controls via [argTypes](https://storybook.js.org/docs/api/arg-types)!

Username is nested in our `ctx` prop, and Storybook controls are meant to correspond with a given prop, so we need to create an array of all the `ctx` possibilities we want to test out. We can then give them each a pretty name, and Storybook will generate a dropdown for us — if we specify a list of options, [Storybook will know to use a dropdown control](https://storybook.js.org/docs/api/arg-types#control).

Let's do it: ```tsx title="src/app/pages/Home.stories.tsx" collapse={1-20} import type { Meta, StoryObj } from "@storybook/react"; import { Home } from "./Home"; const meta: Meta = { component: Home, }; export default meta; type Story = StoryObj; export const NotLoggedIn: Story = { args: { ctx: { user: null, session: null, }, }, }; export const LoggedIn: Story = { args: { ctx: { user: { id: "1", username: "redwood_fan_123", createdAt: new Date(), }, session: null, }, }, // [!code ++:19] argTypes: { ctx: { options: ["redwood_fan_123", "storybook_user", "example_user"], mapping: { redwood_fan_123: { user: { id: "1", username: "redwood_fan_123", createdAt: new Date() }, session: null, }, storybook_user: { user: { id: "2", username: "storybook_user", createdAt: new Date() }, session: null, }, example_user: { user: { id: "3", username: "example_user", createdAt: new Date() }, session: null, }, }, }, }, }; ``` 6. Save it, and go back to our Storybook site. we should see a new dropdown for the `ctx` prop — give it a try!
You did it! 🎉
We now have a fully functional Storybook set up with a component that we can play around with. ## Mocking a dependency that's not a prop [#mocking-a-dependency-thats-not-a-prop] In the previous section, we mocked the `ctx` prop. But what if we want to mock a dependency that isn't a prop? For example, let's say we have a component that makes calls to our database via Prisma. 1. The starter project doesn't ship with a database, so let's imagine we've added one with a `User` table. What's the most obvious thing to do? List out all the users! Let's add this to our `Home` component: ```tsx title="src/app/pages/Home.tsx" // [!code word:async] import { RequestInfo } from "rwsdk/worker"; import { db } from "@/db"; // [!code ++] export async function Home({ ctx }: RequestInfo) { const users = await db.user.findMany(); return (

{ctx.user?.username ? `You are logged in as user ${ctx.user.username}` : "You are not logged in"}

// [!code ++:5]
    {users.map((user) => (
  • {user.username}
  • ))}
); } ``` 2. Now, if we go to our Storybook site, we'll see that it throws an intimidating error. Take a closer look, and we'll see that it's coming from the Prisma client: 3. We need to mock our Prisma client. There are a few ways to do this, and [Storybook](https://storybook.js.org/docs/writing-stories/mocking-data-and-modules/mocking-modules) and [Prisma](https://www.prisma.io/blog/testing-series-1-8eRB5p0Y8o) both have great documentation on this. For the sake of this guide, we're going to do this the most straightforward way. First, we need to create a mocked version of our Prisma client. Create a new file right next to our existing `db.ts` — `src/db.mock.ts`: ```ts title="src/db.mock.ts" /** * First, mock the imported client. */ export let db: unknown; /** * Then, create a function to set the mock client. * We do this so that we can have test-specific mocks, * rather than having only one version of the mocked client. * * @param [dbMock={}] An object to use as the mock client. Be sure to mock any Prisma functions used by the component we're testing. */ export function setupDb(dbMock: unknown = {}) { db = dbMock; } ``` 4. Now, we need to tell Storybook to use this mocked version of the Prisma client. We'll do this using [a Vite alias](https://storybook.js.org/docs/writing-stories/mocking-data-and-modules/mocking-modules#builder-aliases).
(We can instead use [subpath imports](https://storybook.js.org/docs/writing-stories/mocking-data-and-modules/mocking-modules#subpath-imports), but it requires a bit more setup — we'd need to change any existing imports.)
One of the Storybook config files is [`.storybook/main.ts`](https://storybook.js.org/docs/api/main-config/main-config) — this defines the behavior of our Storybook project. Open it up and add the following: ```ts title=".storybook/main.ts" import type { StorybookConfig } from "@storybook/react-vite"; // [!code ++:2] import { mergeConfig } from "vite"; import path from "path"; const config: StorybookConfig = { // [!code ++:8] features: { /** * `experimentalRSC` is required for rendering async server components in Storybook. * It works by wrapping all stories in a Suspense boundary: * https://github.com/storybookjs/storybook/blob/14e18d956fd714c594782fbf23c42765a8b599cd/code/renderers/react/src/entry-preview.tsx#L20-L24 */ experimentalRSC: true, }, stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], addons: [ "@storybook/addon-essentials", "@storybook/addon-onboarding", "@chromatic-com/storybook", "@storybook/experimental-addon-test", ], framework: { name: "@storybook/react-vite", options: {}, }, // [!code ++:9] viteFinal: async (config) => { return mergeConfig(config, { resolve: { alias: { "@/db": path.resolve(__dirname, "../src/db.mock.ts"), }, }, }); }, }; export default config; ``` 5. Every time we edit one of the Storybook configs, we'll need to restart the Storybook server. However, if we do this before we finish mocking the Prisma client, [our component will infinitely re-render](https://github.com/storybookjs/storybook/issues/30317). Let's finish mocking the Prisma client first. Go back to our story, and add the following: ```tsx title="src/app/pages/Home.stories.tsx" collapse={23-66} import type { Meta, StoryObj } from "@storybook/react"; // [!code ++:3] // Must include the `.mock` portion of filename to specify that that's what we want to import import { setupDb } from "@/db.mock"; import { Home } from "./Home"; const meta: Meta = { // [!code ++:12] // https://storybook.js.org/docs/writing-tests/component-testing#beforeeach beforeEach: async () => { setupDb({ user: { findMany: () => [ { id: "1", username: "redwood_fan_123", createdAt: new Date() }, { id: "2", username: "storybook_user", createdAt: new Date() }, { id: "3", username: "example_user", createdAt: new Date() }, ], }, }); }, component: Home, }; export default meta; type Story = StoryObj; export const NotLoggedIn: Story = { args: { ctx: { user: null, session: null, }, }, }; export const LoggedIn: Story = { args: { ctx: { user: { id: "1", username: "redwood_fan_123", createdAt: new Date(), }, session: null, }, }, argTypes: { ctx: { options: ["redwood_fan_123", "storybook_user", "example_user"], mapping: { redwood_fan_123: { user: { id: "1", username: "redwood_fan_123", createdAt: new Date() }, session: null, }, storybook_user: { user: { id: "2", username: "storybook_user", createdAt: new Date() }, session: null, }, example_user: { user: { id: "3", username: "example_user", createdAt: new Date() }, session: null, }, }, }, }, }; ``` 6. Now, restart the Storybook server (`CTRL + C` to stop it), and go back to the Storybook site. We should see our mocked list of users:
## Continued Learning [#continued-learning] You did it! 🚀 We now have a fully functioning Storybook project, and have started to explore the benefits of developing UI in isolation. We're also well on our way to having a robust, well-documented, and well-tested UI component library for our RedwoodSDK project. Some great resources for next steps are: * [Testing UIs with Storybook](https://storybook.js.org/docs/writing-tests/) * [Documenting components with Storybook](https://storybook.js.org/docs/writing-docs/) * [Publishing your Storybook](https://storybook.js.org/docs/sharing) --- # Tailwind CSS (https://docs.rwsdk.com/guides/frontend/tailwind) ## Installing Tailwind [#installing-tailwind] Since the RedwoodSDK is based on React and Vite, we can work through the ["Using Vite" documentation](https://tailwindcss.com/docs/installation/using-vite). 1. Install Tailwind CSS 2. Configure the Vite Plugin {" "} ```ts title="vite.config.mts" import { defineConfig } from "vite"; import tailwindcss from "@tailwindcss/vite"; // [!code ++] import { redwood } from "rwsdk/vite"; import { cloudflare } from "@cloudflare/vite-plugin"; export default defineConfig({ // [!code ++:3] environments: { ssr: {}, }, plugins: [ cloudflare({ viteEnvironment: { name: "worker" }, }), redwood(), tailwindcss() // [!code ++] ], }); ``` 3. Create a `src/app/styles.css` file, and import Tailwind CSS ```css title="src/app/styles.css" @import "tailwindcss"; // [!code ++] ``` 4. Import your CSS and add a `` to the `styles.css` file.
In the `Document.tsx` file, within the `` section, add: ```tsx title="src/app/Document.tsx" import styles from "./styles.css?url"; // [!code ++] ... ... // [!code ++:1] ... ``` 5. To test that Tailwind is working, you'll need to style something in your app. Use the Tailwind CSS docs to understand how to use the utility classes.
For example, you can just pick a random element in your app and add a blue background color to it by adding `className="bg-blue-500"` to it.\` 6. Now, you can run `dev` and the element you styled should look different.
## Customizing TailwindCSS [#customizing-tailwindcss] With Tailwind v4, there is no longer a `tailwind.config.js` file for customizations. Instead, we use the `styles.css` file. All of your customizations should be within a `@theme` block. ```css @import "tailwindcss"; @theme { --color-bg: #e4e3d4; } ``` Now, this custom color can be used: ```tsx

Hello World

``` ## Further reading [#further-reading] * [TailwindCSS](https://tailwindcss.com/) * [VS Code, Tailwind CSS IntelliSense Plugin](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) --- # React Compiler (https://docs.rwsdk.com/guides/optimize/react-compiler) The React Compiler can optimize your components automatically. RedwoodSDK works with the compiler via Vite — you only need to add the Babel plugin and runtime, and enable it in your Vite config. ## Install [#install] First, you'll need to be on the latest release of RedwoodSDK: ```bash pnpm add rwsdk@latest ``` Next, install the React Compiler Babel plugin, and Vite's React plugin. ```bash pnpm add react@latest react-dom@latest react-server-dom-webpack@latest pnpm add -D babel-plugin-react-compiler@latest @vitejs/plugin-react@latest ``` ## Configure Vite [#configure-vite] Enable the compiler by adding the React plugin with the compiler Babel plugin. Place it before the Cloudflare and RedwoodSDK plugins. ```ts title="vite.config.mts" import { defineConfig } from "vite"; import { redwood } from "rwsdk/vite"; import { cloudflare } from "@cloudflare/vite-plugin"; // [!code highlight] import react from "@vitejs/plugin-react"; // [!code ++] export default defineConfig({ plugins: [ // [!code ++:5] react({ babel: { plugins: ["babel-plugin-react-compiler"], }, }), cloudflare({ viteEnvironment: { name: "worker" }, }), redwood(), ], }); ``` If you already have a Vite config, simply add this to your plugins: ```ts react({ babel: { plugins: ["babel-plugin-react-compiler"], }, }), ``` ## Troubleshooting [#troubleshooting] * After enabling, if HMR behaves oddly, clear Vite cache: `rm -rf node_modules/.vite` and restart the dev server. ## Verify Your Setup [#verify-your-setup] Check React DevTools: 1. Install the React Developer Tools browser extension 2. Open your app in development mode 3. Open React DevTools 4. Look for the ✨ emoji next to component names If the compiler is working: * Components will show a “Memo ✨” badge in React DevTools * Expensive calculations will be automatically memoized * No manual `useMemo` is required Source: [React Compiler Installation](https://react.dev/learn/react-compiler/installation) --- # React Server Function Streams (https://docs.rwsdk.com/guides/rsc-streams) This pattern is useful for sending partial responses to the client, such as when you're waiting for data from an external API, like an AI model. ## Example [#example] First create a server function that returns a stream. In this example we're using Cloudflare's AI, and we're streaming the response back to the client. ```tsx title="app/pages/Chat/functions.ts" lineNumbers "use server"; export async function sendMessage(prompt: string) { console.log("Running AI with Prompt:", prompt); const response = await env.AI.run("@cf/meta/llama-4-scout-17b-16e-instruct", { prompt, stream: true, // [!code highlight] }); return response as unknown as ReadableStream; } ``` Now on the client component, we can use the `consumeEventStream` function to parse the chunks whilst keeping the UI updated. ```tsx title="app/pages/Chat/Chat.tsx" lineNumbers "use client"; import { sendMessage } from "./functions"; import { useState } from "react"; import { consumeEventStream } from "rwsdk/client"; export function Chat() { const [message, setMessage] = useState(""); const [reply, setReply] = useState(""); const [isLoading, setIsLoading] = useState(false); const onSubmit = async (e: React.FormEvent) => { e.preventDefault(); setIsLoading(true); setReply(""); // [!code highlight:13] (await sendMessage(message)).pipeTo( consumeEventStream({ onChunk: (event) => { setReply((prev) => { if (event.data === "[DONE]") { setIsLoading(false); return prev; } return (prev += JSON.parse(event.data).response); }); }, }) ); }; return (
{reply}
setMessage(e.target.value)} />
); } ``` For a working example, please see the [Chat example](https://github.com/redwoodjs/example-streaming-ai-chat/tree/main). --- # Troubleshooting (https://docs.rwsdk.com/guides/troubleshooting) ## React Server Components Configuration Errors [#react-server-components-configuration-errors] ### Error: "A client-only module was incorrectly resolved with the 'react-server' condition" [#error-a-client-only-module-was-incorrectly-resolved-with-the-react-server-condition] This error occurs when client-only modules (like `rwsdk/client`, `rwsdk/__ssr`, or `rwsdk/__ssr_bridge`) are being resolved with the `react-server` condition, which they should not be. #### What This Means [#what-this-means] RedwoodSDK uses Node.js package.json [export conditions](https://nodejs.org/api/packages.html#conditional-exports) to ensure the correct code is loaded for each environment: * **Worker environment** (React Server Components): Uses `react-server` condition for server-only modules * **SSR environment**: Does NOT use `react-server` condition * **Client environment**: Uses `browser` condition When client-only modules are incorrectly resolved with `react-server`, it indicates a configuration issue. #### How to Fix [#how-to-fix] 1. **Check your Vite configuration** If you're using RedwoodSDK's `configPlugin`, the resolve conditions are set automatically. However, if you're manually configuring Vite, ensure: ```ts // Worker environment (RSC) resolve: { conditions: ["workerd", "react-server", "module", "node"]; } // SSR environment resolve: { conditions: ["workerd", "module", "browser"]; // Note: NO "react-server" condition } // Client environment resolve: { conditions: ["browser", "module"]; } ``` 2. **Verify you're not overriding resolve conditions** Check your `vite.config.ts` or `vite.config.mts` to ensure you're not manually overriding `resolve.conditions` in a way that conflicts with RedwoodSDK's configuration. ```ts // ❌ Don't do this export default defineConfig({ environments: { ssr: { resolve: { conditions: ["react-server", "workerd"], // Wrong! }, }, }, }); // ✅ Let RedwoodSDK handle it import { configPlugin } from "rwsdk/vite"; export default defineConfig({ plugins: [ configPlugin({ /* ... */ }), ], }); ``` 3. **Check for incorrect imports** Ensure that client-only code is not being imported in server components: ```tsx // ❌ Don't import client-only modules in server components import { initClient } from "rwsdk/client"; // This is client-only! export default function ServerComponent() { // This will cause the error return
Server Component
; } // ✅ Client-only imports should only be in client components ("use client"); import { initClient } from "rwsdk/client"; export default function ClientComponent() { return
Client Component
; } ``` 4. **Intermittent errors (race conditions)** If this error appears intermittently, especially during development server startup, it may indicate a race condition in dependency optimization. This can happen with large libraries. Try: * Restarting the dev server * Clearing Vite's cache: `rm -rf node_modules/.vite` * If the issue persists, it may be a bug in RedwoodSDK - please [file an issue](https://github.com/redwoodjs/sdk/issues)
#### Understanding Export Conditions [#understanding-export-conditions] RedwoodSDK's package.json uses export conditions to route imports correctly: ```json { "exports": { "./client": { "react-server": "./dist/runtime/entries/no-react-server.js", "default": "./dist/runtime/entries/client.js" }, "./worker": { "react-server": "./dist/runtime/entries/worker.js", "default": "./dist/runtime/entries/react-server-only.js" } } } ``` * When `rwsdk/client` is imported with `react-server` condition → throws error (client code shouldn't run in RSC) * When `rwsdk/client` is imported with `default` condition → loads client code ✅ * When `rwsdk/worker` is imported with `react-server` condition → loads worker code ✅ * When `rwsdk/worker` is imported with `default` condition → throws error (server code shouldn't run in client) The build system automatically selects the correct condition based on the environment, but configuration issues can cause the wrong condition to be used. *** ## Directive Scan Errors [#directive-scan-errors] ### Error: "Directive scan failed. This often happens due to syntax errors in files using 'use client' or 'use server'" [#error-directive-scan-failed-this-often-happens-due-to-syntax-errors-in-files-using-use-client-or-use-server] This error occurs during RedwoodSDK's initial scan of your codebase to identify files with `"use client"` and `"use server"` directives. The scan uses esbuild to parse and analyze your files, and it can fail if it encounters syntax errors or other issues. #### What This Means [#what-this-means-1] RedwoodSDK scans all files in your `src` directory to: * Identify which files are client components (`"use client"`) * Identify which files are server functions (`"use server"`) * Build a dependency graph to classify modules correctly * Handle MDX files by compiling them The scan must successfully parse all files to build an accurate picture of your application structure. #### How to Fix [#how-to-fix-1] 1. **Check for syntax errors** The most common cause is syntax errors in files that use directives. Check the error stack trace in the console - it will usually point to the problematic file. Common syntax errors include: * Missing closing braces or parentheses * Incorrect JSX syntax * TypeScript type errors that prevent parsing * Invalid import statements ```tsx // ❌ Missing closing brace "use client"; export function Component() { return
Hello // Missing closing brace and tag } // ✅ Correct syntax "use client"; export function Component() { return
Hello
; } ``` 2. **Check MDX files** If you're using MDX files, ensure they compile correctly. MDX compilation errors can cause the scan to fail. ```mdx ## // ❌ Invalid MDX syntax ## title: My Page #### Understanding the Directive Scan [#understanding-the-directive-scan] The directive scan is a critical part of RedwoodSDK's build process. It: 1. **Scans all files** in your `src` directory (`.ts`, `.tsx`, `.js`, `.jsx`, `.mts`, `.mjs`, `.mdx`) 2. **Identifies directives** by looking for `"use client"` and `"use server"` at the top of files 3. **Builds a dependency graph** by following imports to classify modules as client or server 4. **Handles MDX files** by compiling them with `@mdx-js/mdx` before parsing The scan must complete successfully before your application can build or run. If it fails, the build process stops to prevent incorrect module classification. *** ## Request Context Errors [#request-context-errors] ### Error: "Request context not found. getRequestInfo() can only be called within the request lifecycle" [#error-request-context-not-found-getrequestinfo-can-only-be-called-within-the-request-lifecycle] This error occurs when you try to call `getRequestInfo()` outside of a request context. RedwoodSDK uses Node.js `AsyncLocalStorage` to provide request-scoped data, which is only available during the request lifecycle. #### What This Means [#what-this-means-2] `getRequestInfo()` provides access to request-specific data like: * The incoming `Request` object * Route `params` (e.g., `/users/:id` → `params.id`) * Application `ctx` (context set by middleware) * Response headers and status * RSC (React Server Components) configuration This data is only available when code is running as part of handling an HTTP request. It's not available in: * Module-level code (top-level of files) * Code that runs outside the request lifecycle * **Queue handlers** (background task processing) * **Cron triggers** (scheduled tasks) * Callbacks that execute after the request completes * Code in client components #### How to Fix [#how-to-fix-2] 1. **Use requestInfo as a prop instead** In React Server Components, `requestInfo` is automatically passed as props. Use it directly rather than calling `getRequestInfo()`: ```tsx // ❌ Don't do this in a server component import { getRequestInfo } from "rwsdk/worker"; export default function MyPage() { const requestInfo = getRequestInfo(); // Error! return
{requestInfo.request.url}
; } // ✅ Do this instead - requestInfo is passed as props import type { RequestInfo } from "rwsdk/worker"; export default function MyPage({ request, ctx }: RequestInfo) { const url = new URL(request.url); return
{url.pathname}
; } ``` 2. **In route handlers and middleware** Route handlers and middleware receive `requestInfo` as a parameter: ```tsx // ✅ Route handler - requestInfo is the parameter import { route } from "rwsdk/router"; import type { RequestInfo } from "rwsdk/worker"; route("/users/:id", ({ params, request, ctx }: RequestInfo) => { // Use params, request, ctx directly return ; }); // ✅ Middleware - requestInfo is the parameter function authMiddleware(requestInfo: RequestInfo) { // Access requestInfo directly if (!requestInfo.ctx.user) { return new Response("Unauthorized", { status: 401 }); } } ``` 3. **In server functions ("use server")** Server functions also receive `requestInfo` automatically. You can access it via the `requestInfo` import: ```tsx // ✅ Server function - use requestInfo import "use server"; import { requestInfo } from "rwsdk/worker"; export async function myServerAction() { // requestInfo is available here const { ctx, params } = requestInfo; // ... your code } ``` Note: The `requestInfo` import (not `getRequestInfo()`) works in server functions because they run within the request context. 4. **Avoid calling getRequestInfo() in delayed callbacks** If you need request context in a callback that executes after the request completes (like `setTimeout`, `setInterval`, or promise callbacks that run after the function returns), you need to capture the values you need before the callback: ```tsx // ❌ This won't work "use server"; import { getRequestInfo } from "rwsdk/worker"; export async function myAction() { setTimeout(() => { const info = getRequestInfo(); // Error! Context is lost }, 1000); } // ✅ Capture what you need first ("use server"); import { requestInfo } from "rwsdk/worker"; export async function myAction() { const userId = requestInfo.ctx.user.id; // Capture value setTimeout(() => { // Use the captured value, not getRequestInfo() console.log(userId); }, 1000); } ``` 5. **Don't call getRequestInfo() in client components** Client components run in the browser and don't have access to server-side request context: ```tsx // ❌ This will never work "use client"; import { getRequestInfo } from "rwsdk/worker"; export default function ClientComponent() { const info = getRequestInfo(); // Error! Client components don't have request context return
Client
; } // ✅ Pass data as props instead ("use client"); export default function ClientComponent({ userId }: { userId: string }) { return
User: {userId}
; } // In server component: export default function ServerPage({ ctx }: RequestInfo) { return ; } ``` 6. **Module-level code** Code that runs at the module level (top-level of a file) executes before any request is handled: ```tsx // ❌ This won't work - runs at module load time import { getRequestInfo } from "rwsdk/worker"; const info = getRequestInfo(); // Error! No request context yet export default function Page() { return
Page
; } // ✅ Move it inside a function that runs during request handling import type { RequestInfo } from "rwsdk/worker"; export default function Page({ request }: RequestInfo) { // Access request here, inside the component return
{request.url}
; } ``` 7. **Queue handlers and cron triggers** Queue handlers and scheduled tasks (cron triggers) run outside of the HTTP request lifecycle, so they don't have request context: ```tsx // ❌ This won't work - queue handlers don't have request context import { getRequestInfo } from "rwsdk/worker"; const app = defineApp([ /* routes */ ]); export default { fetch: app.fetch, async queue(batch) { const info = getRequestInfo(); // Error! No request context in queue handler for (const message of batch.messages) { // Process message } }, }; // ✅ Pass data through the message body instead const app = defineApp([ route("/send-email", ({ ctx }: RequestInfo) => { // Capture data from request context env.QUEUE.send({ userId: ctx.user.id, email: ctx.user.email, // ... other data you need }); return new Response("Queued"); }), ]); export default { fetch: app.fetch, async queue(batch) { for (const message of batch.messages) { const { userId, email } = message.body as { userId: number; email: string; }; // Use the data from the message, not request context await sendEmail(email); } }, }; ``` The same applies to cron triggers: ```tsx // ❌ This won't work import { getRequestInfo } from "rwsdk/worker"; export default { fetch: app.fetch, async scheduled(controller) { const info = getRequestInfo(); // Error! No request context in cron // ... scheduled task }, }; // ✅ Cron triggers don't have request context - they're background tasks export default { fetch: app.fetch, async scheduled(controller) { // Do your scheduled work without request context await cleanupOldData(); await generateReports(); }, }; ```
#### Understanding Request Context [#understanding-request-context] RedwoodSDK uses Node.js `AsyncLocalStorage` to provide request-scoped data. This means: 1. **Request context is set** when a request starts (in the router's `handle` method) 2. **Context is available** to all code that runs synchronously within that request 3. **Context is lost** when: * The request completes * Code runs in a different async context (like `setTimeout`, `setInterval`) * Code runs in a client component (browser environment) The `requestInfo` import works in server functions because they're called during the request lifecycle. The `getRequestInfo()` function throws an error if called outside this context to prevent bugs from accessing stale or missing data. --- # Vitest (https://docs.rwsdk.com/guides/vitest) RedwoodSDK supports integration testing using **Vitest** and **Cloudflare Workers Pool**. ## The "Test Bridge" Pattern [#the-test-bridge-pattern] Since tests run in an isolated worker process (powered by `vitest-pool-workers`), they cannot directly access your running application's state or database bindings in the same way a unit test might. To bridge this gap, this guide uses a pattern where the test runner communicates with your worker via a special HTTP route (`/_test`). 1. **Test Side**: Uses an `vitestInvoke` helper to send a POST request with the action name and arguments. 2. **Worker Side**: A `handleVitestRequest` handler receives the request, executes the actual Server Action within the worker's context (with full access to `ctx`, D1, KV, etc.), and returns the result. You can interpret this as "RPC from Test Runner to Worker". ## 1. Configure Vitest [#1-configure-vitest] You will need two things in your `vitest.config.ts`: 1. Use `defineWorkersConfig` from `@cloudflare/vitest-pool-workers/config`. 2. Point the pool to your **built** `wrangler.json`. ```ts // vitest.config.ts import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config"; export default defineWorkersConfig({ test: { include: ["src/**/*.test.{ts,tsx}"], poolOptions: { workers: { wrangler: { // Use the built worker output so `rwsdk/worker` and RSCs resolve correctly. configPath: "./dist/worker/wrangler.json", }, }, }, }, }); ``` ## 2. Setup the Test Bridge [#2-setup-the-test-bridge] Expose a `/_test` route in your `src/worker.tsx` to handle incoming test requests using `rwsdk-community`. You can find a complete working example in our [Vitest Playground](https://github.com/redwoodjs/sdk/tree/main/community/playground/vitest-showcase). ```tsx // src/worker.tsx import { render, route } from "rwsdk/router"; import { defineApp } from "rwsdk/worker"; import { handleVitestRequest } from "rwsdk-community/worker"; import * as appActions from "./app/actions"; import * as testUtils from "./app/test-utils"; export default defineApp([ // ... other middleware // 1. Expose the test bridge route route("/_test", { post: ({ request }) => handleVitestRequest(request, { ...appActions, ...testUtils // Optional: expose specific test utilities }), }), // ... your application routeswe use render(Document, [route("/", Home)]), ]); ``` ## 3. Write a Test [#3-write-a-test] Use the `vitestInvoke` helper from `rwsdk-community/test` to call your exposed actions. ```ts // src/tests/example.test.ts import { expect, it, describe, beforeAll } from "vitest"; import { vitestInvoke } from "rwsdk-community/test"; describe("Integration Test", () => { it("should create an item", async () => { // 1. Call a server action via the bridge const id = await vitestInvoke("createItem", "Test Item"); // 2. Verify result expect(id).toBeGreaterThan(0); // 3. Verify side effects (e.g. ask DB for count) const count = await vitestInvoke("getItemCount"); expect(count).toBe(1); }); }); ``` --- # What is RedwoodSDK? (https://docs.rwsdk.com/) RedwoodSDK is a React framework for Cloudflare. It starts as a Vite plugin that enables server-side rendering, React Server Components, server functions, streaming responses, and real-time capabilities. Its standards-based router—with support for middleware and interrupters—gives you fine-grained control over every request and response. Local development mirrors production with Miniflare, which emulates the Cloudflare runtime. You get access to Durable Objects, D1 (database), R2 (blob storage), Queues, and more—right inside of the box. No installation required. Create a new project by running the following command, replacing {"my-project-name"} with your project name ```bash npx create-rwsdk my-project-name ``` Then ["start developing"](/getting-started/quick-start#start-developing) ## RedwoodSDK is not a framework, it's a "framework" [#redwoodsdk-is-not-a-framework-its-a-framework] RedwoodSDK is not your typical JavaScript framework. We initially resisted calling it a framework. Instead, we described it as a toolkit, or more precisely, an SDK, because we believed most of the heavy lifting should come from the browser and the network, not a JavaScript runtime pretending to be the platform. RedwoodSDK embraces web-native primitives, minimizes abstraction, and gives you full control over the code you write. It is idiomatic to JavaScript and aligned with the platform. Over time, we adopted the term "framework" for clarity and discoverability. But at its core, RedwoodSDK remains a lightweight, composable set of tools that stays out of your way. ## Design Principles [#design-principles] ### 1. Zero Magic [#1-zero-magic] RedwoodSDK avoids all hidden behavior: * No code generation * No transpilation side effects * No special treatment of file names or exports * Only explicit import and export statements * Everything respects JavaScript's core contracts If the runtime relies on convention instead of clarity, it breaks the language contract. With RedwoodSDK, what you write is what runs. ### 2. Composability Over Configuration [#2-composability-over-configuration] RedwoodSDK gives you primitives, not policy: * Build from functions, modules, and types * No opinionated wrappers or rigid folder structures * Prioritizes developer intent and application code * Encourages co-location of logic, UI, and infrastructure You are in control. RedwoodSDK helps you build the software you want without getting in your way. ### 3. Web-First Architecture [#3-web-first-architecture] RedwoodSDK is built for the web as it exists today: * Uses native Web APIs * No abstraction over fetch, Request, Response, or URL * Avoids rebuilding primitives the browser already provides If the platform already gives you a tool, we do not wrap it. We help you use it directly and idiomatically. ## Why This Matters [#why-this-matters] RedwoodSDK is built around a simple idea: stay close to the platform. By minimizing abstraction, it reduces complexity, removes hidden behavior, and makes code easier to understand and maintain. This philosophy drives every architectural decision in RedwoodSDK. It is not just about writing software. It is about understanding the software you are writing. --- # Realtime (Legacy) (https://docs.rwsdk.com/legacy/realtime) The SDK includes built-in support for **realtime updates** using **Cloudflare Durable Objects** and **WebSockets**. With just a few lines of setup, you can enable bidirectional communication between clients and the server — without polling. *** ## Setup [#setup] You'll need to connect three parts: ### 1. Client Setup [#1-client-setup] On the client side, initialize the realtime connection with a `key`. This `key` determines which group of clients should share updates. More on this below. ```ts title="src/client.tsx" import { initRealtimeClient } from "rwsdk/realtime/client"; initRealtimeClient({ key: window.location.pathname, // Used to group related clients }); ``` ### 2. Export the Durable Object [#2-export-the-durable-object] ```ts title="src/worker.tsx" export { RealtimeDurableObject } from "rwsdk/realtime/durableObject"; ``` ### 3. Wire Up the Worker Route [#3-wire-up-the-worker-route] ```ts title="src/worker.tsx" import { realtimeRoute } from "rwsdk/realtime/worker"; import { env } from "cloudflare:workers"; export default defineApp([ realtimeRoute(() => env.REALTIME_DURABLE_OBJECT), // ... your routes ]); ``` ### 4. Add the Durable Object to `wrangler.jsonc` [#4-add-the-durable-object-to-wranglerjsonc] ``` "durable_objects": { "bindings": [ // ... { "name": "REALTIME_DURABLE_OBJECT", "class_name": "RealtimeDurableObject", }, ], }, ``` After updating `wrangler.jsonc`, run `pnpm generate` to update the generated type definitions. *** ## Sync State Hook (Experimental) [#sync-state-hook-experimental] *** ## Understanding Realtime in the SDK [#understanding-realtime-in-the-sdk] React Server Components provide a way of **describing the UI you want to render based the latest app state**. When an event happens (whether user or system-initiated), the app state is updated, the server re-renders the UI, and the client receives the result. In RedwoodSDK, we build on top of this model for real time updates. 1. **An event happens** — user action or external trigger 2. **App state is updated** — using your existing RSC action handlers 3. **Re-render is triggered** — either automatically (as a result of an action) or explicitly via `renderRealtimeClients()` 4. **Each client re-renders** — by calling your server code and receiving the latest state This means you don't need to wire up subscriptions or manually track diffs — just write your app as usual and let the server deliver updated UI to each connected client. *** ## Scoping Updates [#scoping-updates] Each realtime connection is scoped by a `key`. This `key` determines which clients are considered part of the same group, so they can receive the same updates. All clients with the same `key` are connected to the same Durable Object instance. Updates affecting one client are pushed to others in the same group. For example: ```ts initRealtimeClient({ key: "/chat/room-42" }); ``` Only clients in `room-42` will receive updates related to that room. *** ## Client ➝ Server ➝ Client Updates [#client--server--client-updates] For updates that begin from user interaction, consider this example: ```tsx const Note = async ({ ctx }: RequestInfo) => { return ; }; ``` Here, the `ctx` has been populated with the latest content for the relevant note. This is just a normal React Server Component. What's new is that when one client triggers an action, **all other clients** with the same `key` will re-run this server logic too. *** ## Server ➝ Client Updates [#server--client-updates] You can update clients even when the event didn't originate from a user action — for example, background events, notifications, or admin triggers. Use `renderRealtimeClients()` to trigger a re-render for all clients connected to a given `key`: ```ts import { renderRealtimeClients } from "rwsdk/realtime/worker"; import { env } from "cloudflare:workers"; await renderRealtimeClients({ durableObjectNamespace: env.REALTIME_DURABLE_OBJECT, key: "/note/some-id", }); ``` *** ## Why WebSockets and Durable Objects? [#why-websockets-and-durable-objects] To support realtime updates on Cloudflare, WebSockets and Durable Objects are a natural fit. WebSockets give us a persistent, bidirectional connection - not just for pushing updates to the client, but also for sending actions back to the server. Since that connection is already open, we can reuse it for both directions, avoiding the overhead of establishing new HTTP requests. Durable Objects are well-suited for managing these connections: they can **persist across requests**, maintain in-memory state, and coordinate updates between connected clients. ## API Reference [#api-reference] ```ts initRealtimeClient({ key?: string }): Promise ``` Initialises the realtime WebSocket client. * `key`: (optional) Identifies which group of clients this user belongs to. ```ts realtimeRoute((env) => DurableObjectNamespace): RouteDefinition ``` Connects the WebSocket route in your worker to the appropriate Durable Object. ```ts renderRealtimeClients({ durableObjectNamespace, key?: string, }): Promise ``` Triggers a re-render for all clients with a given `key`. * `durableObjectNamespace`: your binding to the Durable Object * `key`: the scope of clients to re-render --- # Migrating from 0.x to 1.x (https://docs.rwsdk.com/migrating) This guide is for users who have an existing RedwoodSDK project and wish to upgrade from a `0.x` version to `1.x`. ## Required Migration Steps [#required-migration-steps] To upgrade your project, you must first take the following steps to avoid breaking changes. ### 1. Upgrade `rwsdk` [#1-upgrade-rwsdk] First, upgrade to the latest version of `rwsdk`: ```sh pnpm add rwsdk@latest ``` ### 2. Update `package.json` Dependencies [#2-update-packagejson-dependencies] In version `1.x`, several core packages like `react` and `wrangler` have been moved to `peerDependencies`. You must now explicitly add them to your project's `package.json`. You can add the required packages by running the following commands in your project root: ```sh pnpm add react@latest react-dom@latest react-server-dom-webpack@latest pnpm add -D @cloudflare/vite-plugin@latest wrangler@latest @cloudflare/workers-types@latest ``` After updating, run `pnpm install` to apply the updates. ### 3. Update `wrangler.jsonc` [#3-update-wranglerjsonc] The Cloudflare Workers runtime now requires a newer compatibility date to support the features used by modern React. * Set the `compatibility_date` in your `wrangler.jsonc` to `2025-08-21` or later. ```jsonc title="wrangler.jsonc" { // ... "compatibility_date": "2025-08-21" // ... } ``` After updating `wrangler.jsonc`, run `pnpm generate` to update the generated type definitions. ### 4. Review Middleware for RSC Action Compatibility [#4-review-middleware-for-rsc-action-compatibility] React Server Component (RSC) actions now run through the global middleware pipeline. Previously, action requests bypassed all middleware. This change allows logic for authentication and session handling to apply consistently. However, if you have existing middleware, it will now execute for RSC actions, which may introduce unintended side effects. You must review your existing global middleware to ensure it is compatible. A new `isAction` boolean flag is now available on the `requestInfo` object passed to middleware, making it easy to conditionally apply logic. **Example:** If you have middleware that should only run for page requests (e.g., logging), you must add a condition to bypass it for action requests: ```typescript title="src/worker.tsx" const loggingMiddleware = ({ isAction, request }) => { // Check if the request is for an RSC action. if (isAction) { // It's an action, so we skip the logging logic. return; } // Otherwise, it's a page request, so we log it. const url = new URL(request.url); console.log('Page requested:', url.pathname); }; export default defineApp([ loggingMiddleware, // ... your other middleware and routes ]); ``` ### 5. Update response header usage [#5-update-response-header-usage] The `headers` property on the request context was removed. Set response headers using `response.headers`. Before: ```typescript const myMiddleware = (requestInfo) => { requestInfo.headers.set('X-Custom-Header', 'my-value'); }; ``` After: ```typescript const myMiddleware = (requestInfo) => { requestInfo.response.headers.set('X-Custom-Header', 'my-value'); }; ``` ### 6. Remove `resolveSSRValue` wrapper [#6-remove-resolvessrvalue-wrapper] The `resolveSSRValue` helper was removed. Call SSR-only functions directly from worker code. Before: ```typescript import { env } from 'cloudflare:workers'; import { resolveSSRValue } from 'rwsdk/worker'; import { ssrSendWelcomeEmail } from '@/app/email/ssrSendWelcomeEmail'; export async function sendWelcomeEmail(formData: FormData) { const doSendWelcomeEmail = await resolveSSRValue(ssrSendWelcomeEmail); const email = formData.get('email') as string; const { data, error } = await doSendWelcomeEmail(env.RESEND_API, email); } ``` After: ```typescript import { env } from 'cloudflare:workers'; import { ssrSendWelcomeEmail } from '@/app/email/ssrSendWelcomeEmail'; export async function sendWelcomeEmail(formData: FormData) { const email = formData.get('email') as string; const { data, error } = await ssrSendWelcomeEmail(env.RESEND_API, email); } ``` ## Optional Refactoring Guide: Adopting the Passkey Addon [#optional-refactoring-guide-adopting-the-passkey-addon] The most significant change in `1.x` is the removal of the `standard` starter in favor of a single, unified `starter` project and the introduction of the officially supported passkey addon. **Your existing authentication code, which was generated from the old `standard` starter, is your own code and will continue to work.** You are not required to change it. The SDK remains backwards-compatible with that implementation. The passkey addon is recommended for **new projects** or projects that have **not yet launched to production**. ### Important Considerations for Migration [#important-considerations-for-migration] The passkey addon uses a **SQLite-based Durable Object** for session and user storage, whereas the old `standard` starter used a **D1 database with Prisma**. Migrating from one to the other is a complex task that requires manually moving data between systems. Because of this complexity, we recommend that existing applications with live user data continue to use their D1/Prisma-based implementation. ### What's Changed? [#whats-changed] * The `standard` starter and its associated tutorial have been removed. * Passkey (WebAuthn) functionality is now provided via a version-locked, downloadable **addon**. This gives you full ownership of the code. * The new implementation uses a lightweight, SQLite-based Durable Object, removing the need for Prisma in the starter. If you decide to migrate, you can follow the [**Authentication Guide**](/experimental/authentication/) to install the passkey addon and adapt it to your existing data models. --- # create-rwsdk (https://docs.rwsdk.com/reference/create-rwsdk) `create-rwsdk` is a command line tool for creating new RedwoodSDK projects ```bash npx create-rwsdk my-project ``` At Redwood, we try to reduce the magic as much as possible. So, even though we have a command line tool for creating new projects, it's important to us that we share what's happening under the hood. At it's core, the `create-rwsdk` command looks at the most recent GitHub release, downloads the attached `tar.gz` file, and extracts it to the current directory. ## Usage [#usage] ```bash npx create-rwsdk [project-name] [options] ``` #### Arguments [#arguments] * `[project-name]`: Name of the project directory to create (optional, will prompt if not provided) #### Options [#options] * `-f, --force`: Force overwrite if directory exists * `--release `: Use a specific release version (e.g., `v1.0.0-alpha.1`) * `--pre`: Use the latest pre-release (e.g., alpha, beta, rc) * `-h, --help`: Display help information * `-V, --version`: Display version number ### Examples [#examples] Create a new project: ```bash npx create-rwsdk my-awesome-app ``` Create a new project with an interactive prompt for the project name: ```bash npx create-rwsdk # You will be prompted: What is the name of your project? ``` Force overwrite an existing directory: ```bash npx create-rwsdk my-awesome-app --force ``` Create a project from the latest pre-release: ```bash npx create-rwsdk my-awesome-app --pre ``` Create a project from a specific release version: ```bash npx create-rwsdk my-awesome-app --release v1.0.0-alpha.10 ``` ## Next steps after creating a project [#next-steps-after-creating-a-project] ```bash cd pnpm install pnpm dev ``` --- # sdk/client (https://docs.rwsdk.com/reference/sdk-client) The `rwsdk/client` module provides a set of functions for client-side operations. ## `initClient` [#initclient] The `initClient` function is used to initialize the React Client. This hydrates the RSC flight payload that's add at the bottom of the page. This makes the page interactive. ### Parameters [#parameters] `initClient()` accepts an optional configuration object: | Parameter | Type | Description | | -------------------- | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `transport` | `Transport` | Custom transport for server communication (defaults to `fetchTransport`) | | `hydrateRootOptions` | `HydrationOptions` | Options passed directly to React's `hydrateRoot`. Supports all React 19 hydration options including error handling (see examples below). | | `handleResponse` | `(response: Response) => boolean` | Custom response handler for navigation errors (navigation GETs) | | `onHydrated` | `(meta?: RscPayloadMeta) => void` | Callback invoked after a new RSC payload has been committed on the client | | `onActionResponse` | `(actionResponse) => boolean \| void` | Optional hook invoked when an action returns a Response; return `true` to signal that the response has been handled and default behaviour should be skipped | ### Error Handling [#error-handling] React 19 introduced powerful error handling APIs that you can use via `hydrateRootOptions`: * **`onUncaughtError`**: Handles uncaught errors (async errors, event handler errors, errors that escape error boundaries) * **`onCaughtError`**: Handles errors caught by error boundaries * **`onRecoverableError`**: Handles recoverable errors during rendering These handlers are **client-side only** and do not handle server-side RSC rendering errors or router-level errors. ### Usage Examples [#usage-examples] ```tsx title="Basic usage" import { initClient } from "rwsdk/client"; initClient(); ``` ```tsx title="With error handling" import { initClient } from "rwsdk/client"; initClient({ hydrateRootOptions: { onUncaughtError: (error, errorInfo) => { console.error("Uncaught error:", error); console.error("Component stack:", errorInfo.componentStack); // Send to monitoring service sendToSentry(error, errorInfo); }, onCaughtError: (error, errorInfo) => { console.error("Caught error:", error); // Handle errors from error boundaries sendToSentry(error, errorInfo); }, }, }); ``` ```tsx title="Integration with Sentry" import { initClient } from "rwsdk/client"; import * as Sentry from "@sentry/browser"; initClient({ hydrateRootOptions: { onUncaughtError: (error, errorInfo) => { Sentry.captureException(error, { contexts: { react: { componentStack: errorInfo.componentStack, errorBoundary: errorInfo.errorBoundary?.constructor.name, }, }, tags: { errorType: "uncaught" }, }); }, onCaughtError: (error, errorInfo) => { Sentry.captureException(error, { contexts: { react: { componentStack: errorInfo.componentStack, errorBoundary: errorInfo.errorBoundary?.constructor.name, }, }, tags: { errorType: "caught" }, }); }, }, }); ``` ```tsx title="Custom error recovery" import { initClient } from "rwsdk/client"; initClient({ hydrateRootOptions: { onUncaughtError: (error, errorInfo) => { // Log error logError(error, errorInfo); // Show user-friendly message showErrorToast("Something went wrong. Please try again."); // Optionally reload the page for critical errors if (isCriticalError(error)) { window.location.reload(); } }, }, }); ``` ```tsx title="With client-side navigation" import { initClient, initClientNavigation } from "rwsdk/client"; const { handleResponse, onHydrated } = initClientNavigation(); initClient({ handleResponse, onHydrated }); ``` ## `initClientNavigation` [#initclientnavigation] The `initClientNavigation` function is used to initialize the client side navigation. An event handler is assocated to clicking the document. If the clicked element contains a link, href, and the href is a relative path, the event handler will be triggered. This will then fetch the RSC payload for the new page, and hydrate it on the client. ## `ClientNavigationOptions` [#clientnavigationoptions] `initClientNavigation()` accepts an optional **`ClientNavigationOptions`** object that lets you control how the browser scrolls after each navigation: | Option | Type | Default | Description | | ---------------- | --------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `scrollToTop` | `boolean` | `true` | Whether to scroll to the top of the page after a successful navigation. Set it to `false` when you want to preserve the existing scroll position (for example, an infinite-scroll list). | | `scrollBehavior` | `'instant' \| 'smooth' \| 'auto'` | `'instant'` | How the scroll *happens* when `scrollToTop` is `true` (ignored otherwise). | | `onNavigate` | `() => Promise \| void` | — | Callback executed **after** the history entry is pushed but **before** the new RSC payload is fetched. Use it to run custom analytics or side-effects. | ### Usage Examples [#usage-examples-1] ```tsx title="Default behaviour – jump to top instantly" import { initClientNavigation } from "rwsdk/client"; initClientNavigation(); ``` ```tsx title="Smooth scrolling to top" initClientNavigation({ scrollBehavior: "smooth", }); ``` ```tsx title="Preserve scroll position" initClientNavigation({ scrollToTop: false, }); ``` ```tsx title="Custom onNavigate logic" initClientNavigation({ scrollBehavior: "auto", onNavigate: async () => { // e.g. send page-view to analytics before RSC fetch starts await myAnalytics.track(window.location.pathname); }, }); ``` ### Rationale & Defaults [#rationale--defaults] RedwoodSDK mirrors the behaviour of classic Multi Page Apps where each link click brings you back to the **top** of the next page. This is the most common expectation and is therefore the default. You can turn it off or make it smooth with a single option – no additional libraries required. ## `NavigationPending` [#navigationpending] `NavigationPending` is a Suspense-aware client component. Put it inside your own `` to hide stale server-backed UI while a matching client-side RSC navigation is pending. ```tsx import { Suspense } from "react"; import { NavigationPending } from "rwsdk/client"; }> ; ``` By default it suspends for any pending navigation. Use `searchParams`, `watch`, or `when` to make a subtree wait only for relevant URL changes. Use one option at a time; if you combine them, `when` takes precedence over `watch`, and `watch` takes precedence over `searchParams`. ### Options [#options] | Prop | Type | Description | | -------------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | | `searchParams` | `readonly string[]` | Shorthand for watching only the listed search params. | | `watch` | `{ pathname?: boolean; searchParams?: boolean \| readonly string[]; hash?: boolean }` | Explicit URL parts to watch. Defaults are `pathname: true`, `searchParams: true`, and `hash: false`. | | `when` | `({ currentUrl, pendingUrl }) => boolean` | Custom predicate. Return `true` when this subtree should suspend for the pending navigation. | ```tsx title="Watch explicit URL parts" }> ``` ```tsx title="Use a custom predicate" }> currentUrl.searchParams.get("tab") !== pendingUrl.searchParams.get("tab") } > ``` The lower-level `useNavigationPending()` hook uses the same options and suspends in the same way. Make sure the `onHydrated` callback returned by `initClientNavigation()` is passed to `initClient()` so pending navigations can resolve after commit. ## `navigate` [#navigate] The `navigate` function is used to programmatically navigate to a new page. It accepts a `href` parameter (the destination URL) and an optional `options` object. | Option | Type | Default | Description | | --------------------- | --------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------- | | `history` | `'push' \| 'replace'` | `'push'` | Determines how the history stack is updated. `'push'` adds a new entry, `'replace'` replaces the current one. | | `info.scrollToTop` | `boolean` | `true` | Whether to scroll to the top of the page after navigation. | | `info.scrollBehavior` | `'instant' \| 'smooth' \| 'auto'` | `'instant'` | How the scroll happens when `scrollToTop` is `true`. | ### Usage Examples [#usage-examples-2] ```tsx title="Basic navigation" import { navigate } from "rwsdk/client"; navigate("/about"); ``` ```tsx title="Navigation with replace" import { navigate } from "rwsdk/client"; navigate("/profile", { history: "replace" }); ``` ```tsx title="Navigation with smooth scroll" import { navigate } from "rwsdk/client"; navigate("/dashboard", { info: { scrollBehavior: "smooth", }, }); ``` --- # sdk/router (https://docs.rwsdk.com/reference/sdk-router) RedwoodSDK's router is a lightweight server-side router that's designed to work with `defineApp` from `rwsdk/worker`. ## `route` [#route] The `route` function is used to define a route. ```ts import { route } from "rwsdk/router"; route("/", () => new Response("Hello, World!")); ``` ### Method-Based Routing [#method-based-routing] The `route` function also accepts a `MethodHandlers` object to handle different HTTP methods on the same path: ```ts route("/api/users", { get: () => new Response(JSON.stringify(users)), post: () => new Response("Created", { status: 201 }), delete: () => new Response("Deleted", { status: 204 }), }); ``` Method handlers can also be arrays of functions for route-specific middleware: ```ts route("/api/users", { get: [isAuthenticated, getUsersHandler], post: [isAuthenticated, validateUser, createUserHandler], }); ``` **Type signature**: ```ts type MethodHandlers = { delete?: RouteHandler; get?: RouteHandler; head?: RouteHandler; patch?: RouteHandler; post?: RouteHandler; put?: RouteHandler; config?: { disable405?: true; disableOptions?: true; }; custom?: { [method: string]: RouteHandler; }; }; ``` **Custom methods**: For non-standard HTTP methods (e.g., WebDAV): ```ts route("/api/search", { custom: { report: () => new Response("Report data"), }, }); ``` **Default behavior**: * OPTIONS requests return `204 No Content` with `Allow` header * Unsupported methods return `405 Method Not Allowed` with `Allow` header * Use `config.disableOptions` or `config.disable405` to opt out ## Important Notes [#important-notes] **HEAD Requests**: Unlike Express.js, RedwoodSDK does not automatically map HEAD requests to GET handlers. You must explicitly define a HEAD handler if you want to support HEAD requests. ```ts route("/api/users", { get: getHandler, head: getHandler, // Explicitly provide HEAD handler }); ``` ## `prefix` [#prefix] The `prefix` function is used to modify the matched string of a group of routes, by adding a prefix to the matched string. This essentially allows you to group related functionality into a seperate file, import those routes and place it into your `defineApp` function. ```ts title="app/pages/user/routes.ts" import { route } from "rwsdk/router"; import { LoginPage } from "./LoginPage"; export const routes = [ route("/login", LoginPage), route("/logout", () => { /* handle logout*/ }), ]; ``` ```ts title="worker.ts" import { prefix } from "rwsdk/router"; import { routes as userRoutes } from "@/app/pages/user/routes"; defineApp([prefix("/user", userRoutes)]); --- This will match `/user/login` and `/user/logout` --- ``` ## render [#render] The `render` function is used to statically render the contents of a JSX element. It cannot contain any dynamic content. Use this to control the output of your HTML. ### Options [#options] The `render` function accepts an optional third parameter with the following options: * **`rscPayload`** (boolean, default: `true`) - Toggle the RSC payload that's appended to the Document. Disabling this will mean that interactivity can no longer work. Your document should not include any client side initialization. * **`ssr`** (boolean, default: `true`) - Enable or disable server-side rendering beyond the 'use client' boundary on these routes. When disabled, 'use client' components will render only on the client. This is useful for client components which only work in a browser environment. NOTE: disabling `ssr` requires `rscPayload` to be enabled. ```tsx import { render } from "rwsdk/router"; import { ReactDocument } from "@/app/Document"; import { StaticDocument } from "@/app/Document"; import { routes as appRoutes } from "@/app/pages/app/routes"; import { routes as docsRoutes } from "@/app/pages/docs/routes"; import { routes as spaRoutes } from "@/app/pages/spa/routes"; export default defineApp([ // Default: SSR enabled with RSC payload render(ReactDocument, [prefix("/app", appRoutes)]), // Static rendering: SSR enabled, RSC payload disabled render(StaticDocument, [prefix("/docs", docsRoutes)], { rscPayload: false }), // Client-side only: SSR disabled, RSC payload enabled render(ReactDocument, [prefix("/spa", spaRoutes)], { ssr: false }), ]); ``` ## `except` [#except] The `except` function defines an error handler that catches errors from subsequent routes, middleware, and RSC actions in the routing tree. Error handlers are searched backwards from where the error occurred, allowing you to create nested error handling with different handlers for different sections of your application. ### Basic Usage [#basic-usage] ```tsx import { except, route } from "rwsdk/router"; import { defineApp } from "rwsdk/worker"; export default defineApp([ except((error) => { console.error(error); return new Response("Something went wrong", { status: 500 }); }), route("/", () => ), route("/api/users", async () => { throw new Error("Database connection failed"); }), ]); ``` ### Returning JSX Elements [#returning-jsx-elements] You can return a React component from an `except` handler to render a custom error page: ```tsx import { except, route } from "rwsdk/router"; import { defineApp } from "rwsdk/worker"; function ErrorPage({ error }: { error: unknown }) { return (

Error

{error instanceof Error ? error.message : "An error occurred"}

); } export default defineApp([ except((error) => { return ; }), route("/", () => ), ]); ``` ### Multiple Handlers and Nesting [#multiple-handlers-and-nesting] You can define multiple `except` handlers in your route tree. The router searches backwards from where the error occurred to find the nearest handler. This allows you to create nested error handling: ```tsx import { except, prefix, route } from "rwsdk/router"; import { defineApp } from "rwsdk/worker"; export default defineApp([ // Global catch-all handler except((error) => { return ; }), prefix("/admin", [ // Specific handler for admin routes except((error) => { if (error instanceof PermissionError) { return new Response("Admin Access Denied", { status: 403 }); } // Return nothing (void) to let it bubble up to the global handler }), route("/dashboard", AdminDashboard), route("/settings", AdminSettings), ]), route("/", Home), ]); ``` In this example: * An error in `/admin/dashboard` is first checked by the admin-specific handler * If the admin handler doesn't handle it (returns `void`), the error bubbles up to the global handler * Errors in other routes (like `/`) are handled by the global handler — the admin handler is path-scoped to `/admin/*` and is skipped for requests outside it If you want a handler that runs for every error (regardless of which route threw), define it at the top level of `defineApp([...])` rather than inside a `prefix(...)`. Top-level `except` handlers are not path-scoped and fire for any route declared after them. ### Error Bubbling [#error-bubbling] If an `except` handler itself throws an error, that new error will bubble up to the next `except` handler further back in the tree: ```tsx export default defineApp([ except((error) => { // This handler catches errors from the inner handler return ; }), except(() => { // This handler throws, so the error bubbles up throw new Error("Handler error"); }), route("/", () => { throw new Error("Route error"); }), ]); ``` ### What Errors Are Caught [#what-errors-are-caught] `except` handlers catch errors from: * **Global middleware**: Errors thrown in middleware functions * **Route handlers**: Errors thrown in route components or functions * **Route-specific middleware**: Errors thrown in middleware arrays * **RSC actions**: Errors thrown during RSC action execution ### Type Signature [#type-signature] ```ts function except( handler: ( error: unknown, requestInfo: T, ) => MaybePromise ): ExceptHandler; ``` *** ## Error Handling [#error-handling] Errors in route handlers and middleware are handled automatically by RedwoodSDK. The framework provides several ways to handle errors: ### Using `ErrorResponse` [#using-errorresponse] The `ErrorResponse` class allows you to return structured errors with status codes: ```ts import { route } from "rwsdk/router"; import { ErrorResponse } from "rwsdk/worker"; route("/api/users/:id", async ({ params }) => { const user = await getUserById(params.id); if (!user) { throw new ErrorResponse(404, "User not found"); } return Response.json(user); }); ``` When an `ErrorResponse` is thrown, RedwoodSDK automatically converts it to an HTTP response with the specified status code and message. ### Try-Catch in Route Handlers [#try-catch-in-route-handlers] You can use try-catch blocks to handle errors in your route handlers: ```ts import { route } from "rwsdk/router"; import { ErrorResponse } from "rwsdk/worker"; route("/api/users", async ({ request }) => { try { const data = await request.json(); const user = await createUser(data); return Response.json(user, { status: 201 }); } catch (error) { if (error instanceof ErrorResponse) { throw error; // Re-throw ErrorResponse to preserve status code } // Handle other errors throw new ErrorResponse(500, "Internal server error"); } }); ``` ### Using `except` for Error Handling [#using-except-for-error-handling] The `except` function is the recommended way to handle errors from Server Components, middleware, and RSC actions. It provides a declarative way to define error handlers that integrate with your routing structure: ```tsx import { except, route } from "rwsdk/router"; import { defineApp } from "rwsdk/worker"; export default defineApp([ except((error) => { // Log error for monitoring console.error("Route error:", error); // Return a user-friendly error page return ; }), route("/", () => ), route("/api/users", async () => { const users = await fetchUsers(); if (!users) { throw new Error("Failed to fetch users"); } return Response.json(users); }), ]); ``` See the [`except` documentation](#except) above for more details on nesting, bubbling, and advanced usage patterns. ### Server-Side Rendering Errors [#server-side-rendering-errors] Errors that occur during React Server Component rendering are automatically caught and handled by `except` handlers if defined. If no `except` handler is found, the error is logged and the request is rejected, which will be caught by the outer error handler in `defineApp`. ### Global Error Handling [#global-error-handling] You can wrap the `fetch` method exposed by `defineApp` to handle all errors globally: ```ts title="src/worker.tsx" import { defineApp, ErrorResponse } from "rwsdk/worker"; import { route } from "rwsdk/router"; const app = defineApp([ route("/", () => ), route("/api/users", async () => { // This might throw an error const users = await fetchUsers(); return Response.json(users); }), ]); export default { fetch: async (request: Request, env: Env, ctx: ExecutionContext) => { try { return await app.fetch(request, env, ctx); } catch (error) { // Handle all unhandled errors globally if (error instanceof ErrorResponse) { return new Response(error.message, { status: error.code }); } if (error instanceof Response) { return error; } // Log error to monitoring service console.error("Unhandled error:", error); // Send to monitoring service asynchronously // Use waitUntil to prevent the worker from being killed // before the async operation completes ctx.waitUntil( sendToMonitoring(error).catch((monitoringError) => { console.error("Failed to send error to monitoring:", monitoringError); }), ); // Return a generic error response return new Response("Internal Server Error", { status: 500 }); } }, }; ``` **Important**: When sending errors to monitoring services (Sentry, DataDog, etc.), these calls are often async. Use `ctx.waitUntil()` from Cloudflare's `ExecutionContext` to ensure the worker doesn't terminate before the async operation completes. Without `waitUntil`, the worker may be killed before the monitoring service receives the error. This pattern allows you to: * Catch all errors that escape route handlers and middleware * Send errors to monitoring services (Sentry, DataDog, etc.) without blocking the response * Return consistent error responses * Prevent unhandled exceptions from reaching Cloudflare Workers ### Unhandled Errors [#unhandled-errors] If an error is thrown that is not an `ErrorResponse` or `Response` instance, and you haven't wrapped the `fetch` method, RedwoodSDK will: 1. Log the error to the console 2. Re-throw the error (which will surface as an unhandled exception in Cloudflare Workers) To prevent unhandled exceptions, you can either: * Wrap the `fetch` method from `defineApp` (recommended for global error handling) * Wrap potentially failing code in try-catch blocks within route handlers and either: * Return an `ErrorResponse` for structured errors * Return a `Response` for custom error responses * Handle the error gracefully within your route handler --- # sdk/worker (https://docs.rwsdk.com/reference/sdk-worker) The `rwsdk/worker` module exports the `defineApp` function, which is the entry point for your Cloudflare Worker. This is the shape of a Cloudflare Worker: ```tsx export default { fetch: (request: Request) => { return new Response("Hello, World!"); }, }; ``` This is the shape of a RedwoodSDK Worker: ```ts import { defineApp } from "rwsdk/worker"; const app = defineApp([/* routes */]); export default { fetch: app.fetch, }; ``` You can also wrap the `fetch` method to add global error handling: ```ts import { defineApp, ErrorResponse } from "rwsdk/worker"; const app = defineApp([/* routes */]); export default { fetch: async (request: Request, env: Env, ctx: ExecutionContext) => { try { return await app.fetch(request, env, ctx); } catch (error) { // Handle all unhandled errors globally if (error instanceof ErrorResponse) { return new Response(error.message, { status: error.code }); } // Send to monitoring service asynchronously // Use waitUntil to prevent the worker from being killed // before the async operation completes ctx.waitUntil( sendToMonitoring(error).catch((monitoringError) => { console.error("Failed to send error to monitoring:", monitoringError); }), ); // Log and return generic error response console.error("Unhandled error:", error); return new Response("Internal Server Error", { status: 500 }); } }, }; ``` **Note**: When sending errors to monitoring services, use `ctx.waitUntil()` to ensure the worker doesn't terminate before async operations complete. For more details on error handling, see the [router error handling documentation](/reference/sdk-router/#error-handling). ## `defineApp` [#defineapp] The `defineApp` function is used to manage how Cloudflare Workers should process requests and subsequently return a response. ```ts import { defineApp } from 'rwsdk/worker' import { route } from 'rwsdk/router' defineApp([ // Middleware function middleware1({ request, ctx }) { ctx.var1 = 'we break' }, function middleware1({ request, ctx }) { ctx.var1 = ctx.var1 + ' abstractions' }, // Route handlers route('/', ({ ctx }) => new Response(ctx.var1)), // we break abstractions route('/ping', () => new Response('pong!')), ]); --- In this example above a request would be processed by the middleware, then the correct route would match and execute the handler. --- ``` ## `ErrorResponse` [#errorresponse] The `ErrorResponse` class is used to return an errors that includes a status code, a message, and a stack trace. You'll be able to extract this information in try-catch blocks, handle it, or return a proper request response. ```ts import { ErrorResponse } from "rwsdk/worker"; export default defineApp([ async ({ ctx, request, response }) => { try { ctx.session = await sessions.load(request); } catch (error) { if (error instanceof ErrorResponse && error.code === 401) { await sessions.remove(request, response.headers); response.headers.set("Location", "/user/login"); return new Response(null, { status: 302, headers: response.headers, }); } throw error; } }, route("/", () => new ErrorResponse(404, "Not Found")), ]); ``` ## `requestInfo: RequestInfo` [#requestinfo-requestinfo] The `requestInfo` object is used to get information about the current request. It's a singleton that's populated for each request, and contains the following information. * `request`: The incoming HTTP [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object * `response`: A [ResponseInit](https://fetch.spec.whatwg.org/#responseinit) object used to configure the status and headers of the response * `ctx`: The app context (same as what's passed to components) * `rw`: RedwoodSDK-specific context * `cf`: Cloudflare's Execution Context API