# Realtime Real-time messaging with channels, presence, and D1-backed persistence. Use WebSocket for live delivery and HTTP for server-side publish/history — no third-party service needed. ## Prerequisites Provision this resource before use. Edge requests without provisioning will error. ### Provision curl -s -X POST https://cohesivity.ai/api/resources/realtime \ -H "Authorization: Bearer " ### Delete curl -s -X DELETE https://cohesivity.ai/api/resources/realtime \ -H "Authorization: Bearer " **Important:** Provision this resource now, before building or running the application. Provisioning is the agent's job, not the application's. ## Common Mistakes - **User ID type mismatch with social-login.** Social-login returns `user.id` as a **number** (e.g., `30`). Realtime presence returns user IDs as **strings** (e.g., `"30"`). Always use `String(user.id)` when minting realtime tokens or comparing presence data with social-login user IDs. - **One channel per WebSocket.** The channel is chosen at connect time via `?channel=`. There is no `subscribe` or `unsubscribe` action in the Worker-based realtime API. - **Confusing `event` (publish) with `action` (receive).** When publishing, you set the `event` field. Recipients and history return it as the `action` field. See "Event/Action Mapping" below. ## What Happens on Provision - Auto-provisions **database** if not already provisioned - Persists messages in your tenant's D1 database (`realtime_messages` table) - You can query that table directly via `POST /edge/database` when you want custom SQL - Video/voice rooms are lazily provisioned on first room creation (powered by Cloudflare RealtimeKit) — no extra setup needed - Deleting the realtime resource drops `realtime_messages` and any RealtimeKit app. You cannot delete **database** while realtime is active. You can choose the D1 write region when provisioning realtime: curl -s -X POST https://cohesivity.ai/api/resources/realtime \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"write_region":"us"}' You can send direct D1 regions (`wnam`, `enam`, `weur`, `eeur`, `apac`, `oc`) or simpler aliases like `us-west`, `us`, `eu`, `eu-east`, `apac`, and `australia`. If omitted, Cohesivity defaults to `apac`. ## Concepts - **Channels** — named pub/sub topics (strings you choose: `chat:room_42`, `feed:user_5`, etc.) - **Events** — when publishing, set `event` to a message type (`message`, `typing`, `update`, etc.). Default: `message`. Recipients see it as `action` on the incoming frame. - **Persist** — messages are saved to D1 by default. Set `persist: false` to skip (use for typing indicators, cursor positions, etc.) - **Presence** — the server returns current occupants on connect and broadcasts join/leave events automatically for that channel ## Connection Token For the fastest repeated edge calls, first mint a short-lived edge bearer token from your server: POST https://cohesivity.ai/edge/session?key= Response: { "token": "", "token_type": "Bearer", "expires_in": 60 } Mint responses are returned with `Cache-Control: private, no-store, max-age=0`. Then mint the WebSocket connection token using that bearer token (recommended) or the raw `?key=` bootstrap path. WebSocket connections require a short-lived token: POST https://cohesivity.ai/edge/realtime/token?key= Content-Type: application/json { "user_id": "alice", "name": "Alice" } Response: `{ "token": "" }` Only `user_id` is required (string or number). `name` is optional (included in presence events). Token expires in 5 minutes. The Worker re-checks that the tenant and `realtime` resource are still active when the WebSocket opens. **Important:** Call this from your server (where the application key lives), not from the browser. The token is what you pass to the client. ## Connect (WebSocket) Connect to exactly one channel per WebSocket: WS wss://cohesivity.ai/edge/realtime?token=&channel=chat:room_42 On connect you receive: { "action": "connected", "channel": "chat:room_42" } { "action": "subscribed", "channel": "chat:room_42", "presence": [...] } User identity comes from the token — it cannot be changed by the client. To watch another live channel, open another WebSocket. ## Publish via WebSocket Send on the connected socket: { "action": "publish", "event": "message", "data": { "text": "hello" } } Recv: { "action": "published", "event": "published", "channel": "chat:room_42", "created_at_ms": 1711111111111 } All subscribers (including the sender) receive the publication: { "action": "message", "event": "message", "channel": "chat:room_42", "data": { "text": "hello" }, "created_at_ms": 1711111111111 } Set `"persist": false` to skip database storage (for ephemeral events like typing): { "action": "publish", "event": "typing", "data": { "user": "Alice" }, "persist": false } ## Presence Events When someone connects to or disconnects from that channel, all other connected clients in the same channel receive: { "action": "join", "channel": "chat:room_42", "user": { "id": "42", "name": "Alice" } } { "action": "leave", "channel": "chat:room_42", "user": { "id": "42", "name": "Alice" }, "last_seen": "..." } Note: In realtime, user IDs are always strings. If your auth returns numeric IDs (e.g. social-login returns `id: 25`), use `String(user.id)` when comparing with presence data. ## Publish (HTTP) Use HTTP to publish from server-side code (Vercel API routes, CF Workers, cron jobs) without a WebSocket connection. Recommended fast path: POST https://cohesivity.ai/edge/session?key= Response: { "token": "", "token_type": "Bearer", "expires_in": 60 } POST https://cohesivity.ai/edge/realtime Authorization: Bearer Content-Type: application/json { "channel": "chat:room_42", "event": "message", "data": { "text": "hello" } } Fallback bootstrap path: POST https://cohesivity.ai/edge/realtime?key= Content-Type: application/json { "channel": "chat:room_42", "event": "message", "data": { "text": "hello" } } Response: `{ "channel": "chat:room_42", "action": "message", "event": "message", "created_at_ms": 1711111111111 }` Set `"persist": false` in the body to skip database storage. ## History Fetch persisted messages for a channel from D1 (scrollback, reconnection catch-up). Recommended fast path: GET https://cohesivity.ai/edge/realtime/history?channel=chat:room_42&after=100&limit=50 Authorization: Bearer Fallback bootstrap path: GET https://cohesivity.ai/edge/realtime/history?channel=chat:room_42&after=100&limit=50&key= Response: `{ "messages": [{ "action": "message", "event": "message", "data": {...}, "id": 101, "created_at_ms": ... }] }` All reads are strongly consistent (always hit the primary D1 database). Parameters: - `channel` (required) — the channel name - `after` — return messages with id greater than this (default: 0, meaning all) - `limit` — max messages to return (default: 50, max: 100) ## Direct SQL (Optional) Because realtime auto-provisions `database`, you can inspect or export the backing table yourself: POST https://cohesivity.ai/edge/database Authorization: Bearer { "query": "SELECT * FROM realtime_messages WHERE channel = ? ORDER BY id DESC LIMIT 100", "params": ["chat:room_42"] } Fallback bootstrap path: POST https://cohesivity.ai/edge/database?key= { "query": "SELECT * FROM realtime_messages WHERE channel = ? ORDER BY id DESC LIMIT 100", "params": ["chat:room_42"] } Schema columns: `id`, `channel`, `event`, `data`, `created_at_ms`. ## Event/Action Mapping When publishing, you set `event`. When receiving (via WebSocket or history), Cohesivity now returns both `action` and `event` with the same value for compatibility: Publishing: { "event": "reaction", ... } → Subscribers receive: { "action": "reaction", "event": "reaction", ... } → History returns: { "action": "reaction", "event": "reaction", ... } If `event` is omitted, it defaults to `"message"`. For new code, key off `event` if you want the most explicit cross-surface field. `action` remains fully supported for backward compatibility. ## Token Lifecycle - The connection token (JWT) is validated when the WebSocket opens - A new WebSocket connect also re-checks that the tenant and `realtime` resource are still active - An established WebSocket connection survives past token expiry — the server does not disconnect you - On disconnect or reconnect, you must mint a **fresh token** (the old one may have expired) - Use `GET /edge/realtime/history?after=` to catch up on messages missed during disconnection ## WebSocket Errors WebSocket protocol errors are sent as `{ "error": "..." }` frames, not Google-style HTTP error envelopes. Common frames: - `{ "error": "Invalid JSON" }` - `{ "error": "Unknown action: \"...\". Use: publish" }` - `{ "error": "publish requires \"data\"" }` - `{ "error": "Failed to persist message: ..." }` ## Common Patterns - **Chat messages** — one WebSocket per open room, persist (default) - **Typing indicator** — `event: "typing"`, `persist: false` - **Online status** — Presence (`action: "join"` / `action: "leave"`, automatic) - **Last seen** — `msg.last_seen` in `leave` action - **Read receipts** — `event: "read"`, `persist: false` - **Notifications** — keep a dedicated `user:` WebSocket open and publish from the server over HTTP - **Live dashboard** — one socket per visible feed, `persist: false` - **Reconnect catch-up** — GET /history?after= on reconnect ## DM Pattern Realtime is channel-based and each WebSocket joins exactly one channel. Recommended pattern: **Channel naming:** Use `dm:_` with sorted IDs so both users compute the same channel name. Example: users 3 and 17 → `dm:3_17`. 1. Each user keeps one WebSocket open to a personal channel: `user:` 2. When User A starts a DM with User B, publish a notification to `user:` with the DM channel name 3. User B receives the notification, opens another WebSocket to the DM channel, and fetches history 4. Both users publish messages to the DM channel normally Example notification (HTTP publish from your server): POST /edge/realtime?key= { "channel": "user:42", "event": "dm_request", "data": { "from": "alice", "dm_channel": "dm:1_42" } } ## Video and Voice (RealtimeKit) Realtime includes video/voice via Cloudflare RealtimeKit. Provision `realtime` once — video is available through `/edge/realtimekit/*`. The `/edge/realtimekit/*` endpoint proxies directly to the Cloudflare RealtimeKit REST API for your tenant. All CF RTK API paths work: POST https://cohesivity.ai/edge/realtimekit/meetings Create a meeting POST https://cohesivity.ai/edge/realtimekit/meetings//participants Add participant → returns auth_token GET https://cohesivity.ai/edge/realtimekit/meetings/ Meeting status PUT https://cohesivity.ai/edge/realtimekit/meetings/ Update meeting GET https://cohesivity.ai/edge/realtimekit/presets List presets The proxy maps `/edge/realtimekit/` to Cloudflare's `api.cloudflare.com/.../realtime/kit/{your_app}/`. Account ID and app ID are injected automatically — just use the paths above. Responses follow Cloudflare's `{ success, data: {...} }` envelope format. The participant auth token is at `data.token`. On the frontend, use the Cloudflare RealtimeKit SDK with the participant `auth_token`. See Cloudflare RealtimeKit docs for SDK setup and packages: https://developers.cloudflare.com/realtime/realtimekit/ The recipient receives: `{ "action": "dm_request", "channel": "user:42", "data": { "from": "alice", "dm_channel": "dm:1_42" } }`, then opens a second WebSocket to `dm:1_42`.