Realtime
Live channels over SSE or WebSocket on your app's own domain — table changes, publishes and fan-out without a socket server.
Realtime is a named channel your users can subscribe to. Changes fan out to every connected client in milliseconds — and there is no socket server for you to run, because the connection terminates at the platform and your app only publishes.
Subscribe
Clients connect on your app's own domain, over server-sent events or a WebSocket:
https://<slug>.camplax.app/v1/realtime/sse?channel=orders:insert
wss://<slug>.camplax.app/v1/realtime/ws?channel=orders:insert
A browser subscribes with one line:
const events = new EventSource(
"https://<slug>.camplax.app/v1/realtime/sse?channel=orders:insert",
);
events.onmessage = (e) => console.log(JSON.parse(e.data));
Subscribers authenticate with a Bearer cx_… project API key, or with the console session cookie — which is how the console's own live viewer works. A browser EventSource cannot set headers, so for end users you either subscribe server-side or expose the stream through a route on your app.
Publish
Anywhere you hold a credential — your server, a cron job, the console's publish button — POST to the notify endpoint:
POST https://<slug>.camplax.app/v1/realtime/notify
Authorization: Bearer <cx_ project key or CAMPLAX_REALTIME_TOKEN>
Content-Type: application/json
{ "channel": "orders:insert", "type": "row", "event": "insert", "row": { "id": 42 } }
Your production app already carries CAMPLAX_REALTIME_TOKEN in its environment — a long-lived capability token that can publish to this project's channels and nothing else, so server code needs no extra key. Preview projects do not get one: previews cannot publish into production channels.
Publish accepts either a channel message (channel plus a row or payload) or a table-change shape (table and op) that fans out to every matching channel.
Table-change triggers
This is the part that makes it feel like the database is live. In the console you create a channel bound to a table and the operations you care about — insert, update, delete, or *. Enabling it installs a trigger on that table in your app's database, and from then on every row change publishes to the channel automatically. Channel names match the binding: orders:insert gets inserts, orders:* gets everything. Disable or delete the channel and the trigger comes out again.
What a client sees
On connect, a {"type":"connected","channel":"…"} hello. Then one JSON message per change:
{ "type": "row", "channel": "orders:insert", "event": "insert", "table": "orders", "row": { "id": 42 }, "at": 1736… }
The stream sends a ping every 25 seconds to keep proxies quiet. If a client falls behind, it is not buffered — it gets the latest message and the intermediate ones are counted as dropped, which the console shows per channel.
In the console
The Realtime page lists channels with live stats — connected clients, messages per minute, today's peak, drops, average fan-out time — and shows both URL templates. Channels are created, toggled and deleted there, or over the API at GET/POST /v1/projects/<slug>/realtime, PATCH/DELETE /realtime/:id, all with your console session.
Under the hood: one Durable Object per channel holds the connections; row changes come from Postgres triggers on your own tables.