Work in Progress - This project is under active development and the API is subject to change.
A proof-of-concept for client-side state management with Cloudflare Durable Objects, featuring optimistic updates and real-time synchronization via WebSockets.
This project is experimental. The declarative collection client API is still being refined.
- Type-safe RPC client for Durable Objects
- Declarative method mapping for CRUD operations
- Optimistic updates with TanStack DB
- Real-time sync via WebSocket subscriptions
- Automatic reconnection with exponential backoff
# Install dependencies
pnpm install
# Run locally (builds app first, then starts both servers)
pnpm dev:allimport { DurableObject } from 'cloudflare:workers';
import { createRpcServer, type RpcFromTarget } from './lib';
export type Todo = {
id: string;
title: string;
completed: boolean;
createdAt: string;
};
export class TodoDurableObject extends DurableObject<Env> {
private rpcServer = createRpcServer(this);
private todos = new Map<string, Todo>();
async listTodos() {
return Array.from(this.todos.values());
}
async createTodo(todo: Todo) {
this.todos.set(todo.id, todo);
return todo;
}
async setCompleted(id: string, completed: boolean) {
const todo = this.todos.get(id);
if (!todo) throw new Error('Todo not found');
const updated = { ...todo, completed };
this.todos.set(id, updated);
return updated;
}
async deleteTodo(id: string) {
this.todos.delete(id);
}
fetch(request: Request): Response {
return this.rpcServer.fetch(request);
}
}
// Export the RPC type for client-side usage
export type TodoRpc = RpcFromTarget<TodoDurableObject, 'fetch'>;import { useMemo } from 'react';
import { match } from 'ts-pattern';
import { useLiveQuery } from '@tanstack/react-db';
import { useQueryClient } from '@tanstack/react-query';
import { createDOClient, createDeclarativeDoCollectionClient } from './doClient';
import type { Todo, TodoRpc } from '../../src/index';
export const App = () => {
const queryClient = useQueryClient();
const myDO = createDOClient<TodoRpc>({ baseUrl: '/do' });
const { collection, actions } = useMemo(
() =>
createDeclarativeDoCollectionClient<
TodoRpc,
Todo,
string,
'listTodos',
'createTodo',
'setCompleted',
'deleteTodo'
>(myDO, queryClient, {
id: 'todos',
queryKey: ['todos'],
getKey: (item) => item.id,
methods: {
list: 'listTodos',
insert: ['createTodo'],
update: ['setCompleted'],
remove: ['deleteTodo'],
},
applyInsert: (action) =>
match(action)
.with({ type: 'createTodo' }, ({ params: [todo] }) => todo)
.exhaustive(),
applyUpdate: (draft, action) => {
match(action)
.with({ type: 'setCompleted' }, ({ params: [, completed] }) => {
draft.completed = completed;
})
.exhaustive();
},
}),
[queryClient],
);
// Query the collection
const { data: todos = [] } = useLiveQuery(collection);
// Mutations are fully typed
const handleAdd = (todo: Todo) => {
actions.insert({ type: 'createTodo', params: [todo] });
};
const handleToggle = (id: string, completed: boolean) => {
actions.update({ type: 'setCompleted', params: [id, completed] });
};
const handleDelete = (id: string) => {
actions.delete({ type: 'deleteTodo', params: [id] });
};
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => handleToggle(todo.id, !todo.completed)}
/>
{todo.title}
<button onClick={() => handleDelete(todo.id)}>Delete</button>
</li>
))}
</ul>
);
};Creates a WebSocket-based RPC server from a Durable Object instance. Automatically exposes all public methods as RPC endpoints.
Creates a type-safe RPC client that connects to a Durable Object via WebSocket.
const client = createDOClient<TodoRpc>({ baseUrl: '/do' });
const todoClient = client('my-todo-list');
// Direct RPC calls
const todos = await todoClient.listTodos();
await todoClient.createTodo({ id: '1', title: 'Hello', completed: false });
// Subscribe to method events
const unsubscribe = todoClient.subscribe('createTodo', (event) => {
console.log('Todo created:', event.result);
});
// Connection status
todoClient.onStatusChange((status) => {
console.log('Connection:', status); // 'connecting' | 'connected' | 'disconnected'
});Creates a collection client with declarative method mapping for CRUD operations. Integrates with TanStack DB for optimistic updates and TanStack Query for caching.
Config options:
id- Durable Object instance IDqueryKey- TanStack Query cache keygetKey- Function to extract the item keymethods- Maps CRUD operations to RPC method namesapplyInsert- Returns the item to insert from an actionapplyUpdate- Mutates the draft for optimistic updatesonInsert/onUpdate/onDelete- Server event handlers
src/
├── lib/
│ ├── index.ts # All exports (server + client)
│ ├── client.ts # Client-only exports (browser-safe)
│ ├── rpc.ts # Shared types
│ ├── rpc-server.ts # createRpcServer (Cloudflare Workers)
│ └── rpc-client.ts # createDOClient (browser)
└── index.ts # Durable Object + Worker
app/src/
├── doClient.ts # Collection client utilities
└── App.tsx # Example React app