Using Realtime
WebSocket connections, authentication, and client-side event handling.
This page covers connecting to the Quickback realtime system from client applications — authentication, subscribing to events, and handling messages.
Connecting
Open a WebSocket connection to the realtime worker:
const ws = new WebSocket("wss://api.yourdomain.com/realtime/v1/websocket");Authentication
After connecting, send an auth message to authenticate. Two methods are supported.
Session Token (Browser/App)
ws.onopen = () => {
ws.send(JSON.stringify({
type: "auth",
token: sessionToken, // JWT from Better Auth session
organizationId: activeOrgId,
}));
};API Key (Server/CLI)
ws.onopen = () => {
ws.send(JSON.stringify({
type: "auth",
token: apiKey, // API key for machine-to-machine auth
organizationId: orgId,
}));
};Auth Response
On success:
{
"type": "auth_success",
"organizationId": "org_123",
"userId": "user_456",
"role": "admin",
"roles": ["admin", "member"],
"authMethod": "session"
}On failure, the connection is closed with an error message.
Handling Messages
CRUD Events
CRUD events use the postgres_changes type:
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === "postgres_changes") {
const { table, eventType, new: newRecord, old: oldRecord } = msg;
switch (eventType) {
case "INSERT":
addRecord(table, newRecord);
break;
case "UPDATE":
updateRecord(table, newRecord);
break;
case "DELETE":
removeRecord(table, oldRecord.id);
break;
}
}
};Event payload:
{
"type": "postgres_changes",
"table": "claims",
"schema": "public",
"eventType": "INSERT",
"new": { "id": "clm_123", "title": "Breaking News", "status": "pending" },
"old": null
}For UPDATE events, both new and old are populated. For DELETE events, only old is populated.
Custom Broadcasts
Custom events use the broadcast type:
if (msg.type === "broadcast") {
const { event, payload } = msg;
if (event === "processing-complete") {
refreshMaterial(payload.materialId);
} else if (event === "extraction:progress") {
updateProgressBar(payload.percent);
}
}Event payload:
{
"type": "broadcast",
"event": "processing-complete",
"payload": {
"materialId": "mat_123",
"claimCount": 5,
"duration": 1234
}
}Custom namespaces (from defineRealtime()) use the format namespace:event — e.g., extraction:started, extraction:progress.
Security
Role-Based Filtering
Events are only delivered to users whose role matches the broadcast's targetRoles. If a table's realtime config specifies requiredRoles: ["admin", "member"], only users with those roles receive the events.
Per-Role Masking
Field values are masked according to the subscriber's role. For example, with this masking config:
masking: {
ssn: { type: "ssn", show: { roles: ["admin"] } },
}- Admin sees:
{ ssn: "123-45-6789" } - Member sees:
{ ssn: "*****6789" }
Masking is pre-computed per-role (O(roles), not O(subscribers)) for efficiency.
User-Specific Events
Events can target a specific user within an organization. Only that user receives the broadcast — all other connections in the org are skipped.
Organization Isolation
Each organization has its own Durable Object instance. Users can only subscribe to events in organizations they belong to, enforced during authentication.
Reconnection
WebSocket connections can drop due to network issues. Implement reconnection logic in your client:
function connect() {
const ws = new WebSocket("wss://api.yourdomain.com/realtime/v1/websocket");
ws.onopen = () => {
ws.send(JSON.stringify({
type: "auth",
token: sessionToken,
organizationId: activeOrgId,
}));
};
ws.onclose = () => {
// Reconnect after delay
setTimeout(connect, 2000);
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
handleMessage(msg);
};
return ws;
}Required Environment Variables
| Variable | Description |
|---|---|
REALTIME_URL | URL of the broadcast/realtime worker |
ACCESS_TOKEN | Internal service-to-service auth token |
# wrangler.toml
[vars]
REALTIME_URL = "https://my-app-broadcast.workers.dev"
ACCESS_TOKEN = "your-internal-secret-token"Cloudflare Only
Realtime requires Cloudflare Durable Objects and is only available with the Cloudflare runtime.
See Also
- Durable Objects Setup — Configuration, event formats, masking, and custom namespaces
- Masking — Field masking configuration