Realtime
Real-time updates via WebSockets with Cloudflare Durable Objects.
Quickback ships two parallel realtime primitives, both backed by Cloudflare Durable Objects:
/broadcast/v1/*— server fan-out. One DO instance per subscription scope. Postgres-changes events from CRUD routes plus custom broadcast events flow out to every subscriber, with role filtering and per-role field masking applied. Best for "tell every client in this shared scope that something changed." The scope can be an organization, a specific user, or a compiler-resolved resource key likeevents:evt_123. Documented on this page./realtime/<binding>/<room-id>— bidirectional rooms. One DO instance per room (per document, per interview, per game). Client↔client messages, presence, and CRDT-friendly state. Use these for collaborative editing. See PartyServer rooms.
Both share the same auth model and ride on the same compiled worker; they solve different problems.
Architecture
┌──────────────────────────────────────────┐
│ Quickback Worker │
│ │
│ ┌──────────┐ DO binding ┌────────────────────┐
│ │ Hono API │ ──────────────► │ Broadcaster (DO) │
│ │ │ │ WebSocket manager │
│ └──────────┘ └─────────┬──────────┘
│ │
└─────────────────────────────────────────┼┘
│ WebSocket
│
┌────────▼─────────┐
│ Browser Clients │
│ (CMS, Account, │
│ Admin, Custom) │
└──────────────────┘The Broadcaster Durable Object runs inline in your main Quickback worker — no separate worker deployment needed. The API calls the DO directly via its binding (in-process, no network hop).
- Quickback Worker — Your compiled API with the Broadcaster DO class exported from the same worker.
- Broadcaster (Durable Object) — Manages WebSocket connections per subscription scope. One instance per
scopeKey, organization, or user lane. - Browser Clients — CMS, Account, Admin, and custom frontends connect via WebSocket on the same origin.
Key Features
| Feature | Description |
|---|---|
| Scope-aware fan-out | Routes by scopeKey, organizationId, or userId |
| Role-based filtering | Only send events to users with matching roles |
| Per-role masking | Different users see different field values based on their role |
| User-specific targeting | Send events to a specific user within an org |
| Resource-scoped tickets | Reuse authz relationship roles to mint scope-bound ws tickets |
| Custom broadcasts | Arbitrary events beyond CRUD |
| Custom namespaces | defineRealtime() for type-safe event helpers |
| Ticket-based auth | HMAC-signed tickets verified at WebSocket upgrade — no HTTP round-trip |
Enabling Realtime
Add realtime to individual table definitions:
export default defineTable(applications, {
firewall: [{ field: 'organizationId', equals: 'ctx.activeOrgId' }],
realtime: {
enabled: true,
onInsert: true,
onUpdate: true,
onDelete: true,
requiredRoles: ["recruiter", "hiring-manager"],
fields: ["id", "candidateId", "stage"],
},
});And enable the realtime binding in your database config. The compiler generates the Durable Object class, helper functions, and wrangler bindings — all within your main worker.
providers: {
database: defineDatabase("cloudflare-d1", {
realtime: {
wsTicket: {
role: "attendee",
requestField: "eventId",
scopeTable: "events",
},
},
}),
}With that config, /broadcast/v1/ws-ticket can authenticate the caller, verify the declared authz relationship role against body.eventId, and mint a short-lived ticket scoped to the resolved events:<id> channel.
Pages
- Durable Objects Setup — Broadcaster configuration, wrangler bindings, event formats, masking, and custom namespaces
- Using Realtime — Subscribing to
/broadcast/v1/*: WebSocket connection, ticket auth, client-side handling - PartyServer Rooms — Per-room collab via
/realtime/<binding>/<room-id>: presence, live notes, room IDs