Use when building realtime features with rwsdk/RedwoodSDK on Cloudflare - covers WebSocket setup, Durable Objects configuration, bidirectional client-server updates, and scoped realtime groups. Triggers include collaborative editing, live updates, multi-user sync, or any feature needing push updates without polling.
Two patterns for realtime updates in rwsdk:
| Pattern | Use Case | Granularity |
|---|---|---|
| Page-wide RSC | Full page re-renders on any change | Entire route |
| useSyncedState | Individual state values synced | Per-value |
Both use Cloudflare Durable Objects + WebSockets. No polling.
Re-renders the entire page for all connected clients when state changes. Best for collaborative documents, dashboards, or any page where all clients should see the same live view.
import { initRealtimeClient } from "rwsdk/realtime/client";
initRealtimeClient({ key: window.location.pathname });
The key groups clients—all clients with matching keys receive the same updates.
// src/worker.tsx
export { RealtimeDurableObject } from "rwsdk/realtime/durableObject";
import { realtimeRoute } from "rwsdk/realtime/worker";
import { env } from "cloudflare:workers";
export default defineApp([
realtimeRoute(() => env.REALTIME_DURABLE_OBJECT),
// ... your routes
]);
{
"durable_objects": {
"bindings": [
{
"name": "REALTIME_DURABLE_OBJECT",
"class_name": "RealtimeDurableObject",
},
],
},
}
Run pnpm generate after updating wrangler.jsonc.
Routes using realtime must use the realtime-enabled Document. Import from rwsdk/realtime:
import { Document } from "rwsdk/realtime/Document";
const App = ({ children }) => (
<Document>
<html>
<body>{children}</body>
</html>
</Document>
);
This is the core mechanism for server-push updates. Use renderRealtimeClients whenever you need to push updates to clients from server-side events:
import { renderRealtimeClients } from "rwsdk/realtime/worker";
import { env } from "cloudflare:workers";
// Push update to all clients watching this key
await renderRealtimeClients({
durableObjectNamespace: env.REALTIME_DURABLE_OBJECT,
key: "/note/some-id",
});
When to use renderRealtimeClients:
Without calling renderRealtimeClients, clients won't see server-side changes until they trigger an action themselves.
renderRealtimeClients() explicitlykey receive updated UISyncs individual state values across clients. Like useState but bidirectional with the server. Best for granular shared state without full page re-renders.
// src/worker.tsx
import { env } from "cloudflare:workers";
import {
SyncedStateServer,
syncedStateRoutes,
} from "rwsdk/use-synced-state/worker";
import { defineApp } from "rwsdk/worker";
export { SyncedStateServer };
export default defineApp([
...syncedStateRoutes(() => env.SYNCED_STATE_SERVER),
// ... your routes
]);
{
"durable_objects": {
"bindings": [
{
"name": "SYNCED_STATE_SERVER",
"class_name": "SyncedStateServer",
},
],
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["SyncedStateServer"],
},
],
}
Run pnpm generate after updating.
"use client";
import { useSyncedState } from "rwsdk/use-synced-state/client";
export const SharedCounter = () => {
// Args: initialValue, key, roomId (optional)
const [count, setCount] = useSyncedState(0, "counter");
return <button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>;
};
Isolate state to specific groups:
const [messages, setMessages] = useSyncedState<string[]>(
[],
"messages",
roomId,
);
Different room IDs = isolated state. Users in room-1 won't see room-2 updates.
Transform keys or rooms on the server for auth-based scoping:
// Scope user-prefixed keys to current user
SyncedStateServer.registerKeyHandler(async (key, stub) => {
const userId = requestInfo.ctx.userId;
if (key.startsWith("user:")) {
return `${key}:${userId}`;
}
return key;
});
// Transform room IDs
SyncedStateServer.registerRoomHandler(async (roomId, reqInfo) => {
if (roomId === "private" && reqInfo?.ctx?.userId) {
return `user:${reqInfo.ctx.userId}`;
}
return roomId ?? "syncedState";
});
State is in-memory by default. Add persistence:
SyncedStateServer.registerSetStateHandler((key, value) => {
// Save to database
});
SyncedStateServer.registerGetStateHandler((key, value) => {
// Load from database if value undefined
});
| Consideration | Page-wide RSC | useSyncedState |
|---|---|---|
| Update scope | Entire page | Individual values |
| Re-render cost | Higher (full RSC) | Lower (state only) |
| Server logic | Runs on every update | Client-side updates |
| Best for | Collaborative docs, dashboards | Counters, presence, forms |
Combine both: Use page-wide realtime for the main view, useSyncedState for ephemeral UI state (typing indicators, cursor positions).
| Function | Purpose |
|---|---|
initRealtimeClient({ key? }) | Initialize WebSocket. key scopes client group. |
realtimeRoute((env) => namespace) | Connect route to Durable Object. |
renderRealtimeClients({ durableObjectNamespace, key? }) | Push re-render to all clients in key group. |
| Function | Purpose |
|---|---|
useSyncedState(initial, key, roomId?) | Synced state hook. |
syncedStateRoutes(() => namespace) | Register synced state routes. |
SyncedStateServer.registerKeyHandler(fn) | Transform keys server-side. |
SyncedStateServer.registerRoomHandler(fn) | Transform room IDs server-side. |
SyncedStateServer.registerSetStateHandler(fn) | Hook into state updates. |
SyncedStateServer.registerGetStateHandler(fn) | Hook into state retrieval. |