This demo shows how to run Zero in a Cloudflare Workers + Durable Objects environment. It demonstrates:
- A React web UI using Zero
- Using Hono to implement Zero's API requirements and auth
- A Durable Object running Zero as another client for live monitoring
Imagine you're running collaboration sessions in DOs and need to reliably control their lifecycle. Instead of unreliably sending shutdown messages to every DO, you can write state to Postgres and have the DOs sync that state. The DO monitors what state it should be in and acts accordingly.
More generally, any time a DO needs some subset of Postgres data, it's useful to have a live-updated, consistent view rather than repeatedly querying.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Browser (http://localhost:5173) β
β βββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β React UI + Zero Client β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Zero Cache (http://localhost:4848) β
β - Proxies queries to Worker endpoints β
β - Manages replica state β
β - Streams changes from Postgres β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββΌβββββββββββββββββ
β β β
ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ
β /api/ β β /api/ β β /api/ β
β get-queries β β mutate β β do/init β
ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ
β β β
ββββββββββββββββββΌβββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Cloudflare Worker (via Vite Plugin) β
β - Hono server handling API routes β
β - Durable Object (ZeroDO) β
β ββ Zero Client monitoring messages β
β ββ Prints live table to console β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Postgres (localhost:5432) β
β - Source of truth β
β - Logical replication enabled β
β - User and Message tables β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
You'll need 3 terminals to run this example!
# Terminal 1
npm run dev:db-up# Terminal 2
npm run dev:zero-cache# Terminal 3
npm run dev:uiThe Vite dev server (with Cloudflare plugin) runs your Worker code locally, handling both the React UI and API endpoints.
Open a browser at http://localhost:5173 to:
- Add/edit/delete messages
- Login/logout (randomly assigns you a user)
- Filter messages by sender or text content
Once the UI is running, trigger the DO to start monitoring messages:
curl http://localhost:5173/api/do/initOr open http://localhost:5173/api/do/init in your browser.
The DO will start printing a live-updating table of messages to Terminal 3 (where dev:ui is running). As you add, edit, or delete messages in the web UI, you'll see the DO's console output update in real-time!
hello-zero-cf/
βββ src/
β βββ shared/ # Code shared between client and server
β β βββ schema.ts # Zero schema (User, Message tables)
β β βββ queries.ts # Synced queries
β β βββ mutators.ts # Custom mutators (create, delete, update)
β β βββ auth.ts # Shared auth constants
β β βββ must.ts # Utility for null checking
β βββ worker/ # Cloudflare Worker code
β β βββ index.ts # Hono app with routes
β β βββ zero-do.ts # Durable Object with Zero client
β β βββ login.ts # Authentication handlers
β β βββ mutate.ts # Mutation endpoint
β β βββ get-queries.ts # Query endpoint
β βββ react-app/ # React UI
β βββ App.tsx # Main app component
β βββ main.tsx # Entry point with ZeroProvider
β βββ ...
βββ db/
β βββ docker-compose.yml # Postgres with replication config
β βββ seed.sql # Database schema and seed data
βββ wrangler.json # Cloudflare Workers config
βββ .env # Environment variables
The demo uses Zero's synced queries API (not legacy ad-hoc queries):
// src/shared/queries.ts
export const queries = {
users: syncedQuery("users", z.tuple([]), () => {
return builder.user;
}),
messages: syncedQuery("messages", z.tuple([]), () => {
return builder.message.related("sender").orderBy("timestamp", "desc");
}),
filteredMessages: syncedQuery(
"filteredMessages",
z.tuple([z.object({ senderID: z.string(), body: z.string() })]),
({ senderID, body }) => {
let query = builder.message.related("sender");
if (senderID) query = query.where("senderID", senderID);
if (body) query = query.where("body", "LIKE", `%${escapeLike(body)}%`);
return query.orderBy("timestamp", "desc");
}
),
};Server-side mutators with authentication checks:
// src/shared/mutators.ts
export function createMutators(userID?: string) {
return {
message: {
async create(tx: Transaction<Schema>, message: Message) {
await tx.mutate.message.insert(message);
},
async delete(tx: Transaction<Schema>, id: string) {
mustBeLoggedIn(userID);
await tx.mutate.message.delete({ id });
},
async update(tx: Transaction<Schema>, message: MessageUpdate) {
mustBeLoggedIn(userID);
const prev = await tx.query.message.where("id", message.id).one().run();
if (!prev) return;
if (prev.senderID !== userID) {
throw new Error("Must be sender of message to edit");
}
await tx.mutate.message.update(message);
},
},
};
}Uses Hono's signed cookies (no JWT library needed):
// Server: src/worker/login.ts
await setSignedCookie(c, AUTH_COOKIE_NAME, userID, secretKey);
// Client: src/react-app/main.tsx
const signedCookie = Cookies.get(AUTH_COOKIE_NAME);
const userID = signedCookie && signedCookie.split(".")[0];The DO runs Zero's lower-level class API (not React hooks):
// src/worker/zero-do.ts
export class ZeroDO extends DurableObject {
#z: Zero<Schema> = new Zero({
server: "http://localhost:4848",
userID: "anon",
schema,
kvStore: "mem",
});
constructor(state: DurableObjectState, env: Env) {
super(state, env);
const view = this.#z.materialize(queries.messages());
view.addListener(this.#render);
}
#render = (messages) => {
// Print live-updating table to console
};
}To stop Postgres and remove volumes:
npm run dev:db-downTo completely clean the database and Zero replica files:
npm run dev:cleanThis is a development demo. For production deployment to Cloudflare:
- Create a production Postgres database with logical replication enabled
- Set up environment variables in Cloudflare dashboard or via
wrangler secret - Deploy:
npm run deploy
Note: You'll need to host the Zero cache server somewhere accessible to your Worker (Cloudflare Workers can make outbound HTTP requests).