# 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` (legacy receive).** When publishing, you set the `event` field. Recipients and history now return both `event` and `action` with the same value. 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`. > **Server-side only.** `coh_application_key` is a secret. Call this from your `vercel-hosting` API routes, `cloudflare-workers`, or your own server tier — never from a browser, mobile app, or other client-side code. See the canonical key-secrecy directive in `.cohesivity` for details. ## 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 both `event` and the legacy-compatible `action` on incoming frames. - **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", "ttl": 300 } Response: `{ "token": "", "expires_in": 300 }` Fields: - `user_id` (required) — string or number identifying the user - `name` (optional) — display name included in presence events - `ttl` (optional) — token lifetime in seconds (integer, 60–3600). Defaults to 300 (5 minutes). For chat-style apps where users sit on a page for hours, mint with `ttl: 1800` or `3600`. Existing WebSocket connections do not auto-refresh — reconnect with a fresh token before expiry. 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 only 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 when the token expires - Token expiry controls the window during which the token can be used to open a new WebSocket connection, not the lifetime of an already-open connection - On disconnect or reconnect, mint a **fresh token** (the old one may have expired). Use `ttl` to request a longer validity window (up to 3600s) if needed. - 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" } } The recipient receives: `{ "action": "dm_request", "event": "dm_request", "channel": "user:42", "data": { "from": "alice", "dm_channel": "dm:1_42" } }`, then opens a second WebSocket to `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 the upstream participant 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. For participant creation, use `data.token` from the response as the participant auth token. ### Eventual consistency — poll after create After creating a meeting via `POST /edge/realtimekit/meetings`, poll `GET /edge/realtimekit/meetings/:id` until it resolves before minting participants. Meeting creation is eventually consistent — calling participant endpoints with the freshly-returned `id` may briefly return `ResourceNotFound`. A simple loop (e.g., 5 attempts at 200ms apart) is sufficient. On the frontend, use the Cloudflare RealtimeKit SDK with the participant token from `data.token`. **Frontend packages:** - React: `@cloudflare/realtimekit-react` + `@cloudflare/realtimekit-react-ui` - Web Components: `@cloudflare/realtimekit-web` + `@cloudflare/realtimekit-ui` - Angular: `@cloudflare/realtimekit-angular` + `@cloudflare/realtimekit-angular-ui` - React Native: `@cloudflare/realtimekit-react-native-ui` The UI Kit provides a complete meeting experience out of the box (video grid, controls, setup screen). Quick start with React: ``` import { RtkMeeting } from '@cloudflare/realtimekit-react-ui'; ``` **Docs:** - RealtimeKit overview: https://developers.cloudflare.com/realtime/realtimekit/ - UI Kit (pre-built components): https://developers.cloudflare.com/realtime/realtimekit/ui-kit - Core SDK (build custom UI): https://developers.cloudflare.com/realtime/realtimekit/core - Examples: https://github.com/cloudflare/realtimekit-web-examples - Live demo: https://demo.realtime.cloudflare.com ## Launch Rate Limits Ephemeral tenants pause as a whole if any authoritative hard cap below is exceeded. Claimed tiers use account-scoped buckets shared across every project owned by the Cohesivity user; OpenAI, Deepgram, and Exa are fluid-only after tier, rate, and concurrency checks; Deepgram has no fixed monthly usage bucket for claimed tiers. ### Realtime chat / presence / history **Ephemeral** - token mints: 100 per ephemeral tenant lifetime before claim or expiry - published messages: 25000 per ephemeral tenant lifetime before claim or expiry - history reads: 2500 per ephemeral tenant lifetime before claim or expiry - concurrent sockets: 5 max at once - requests: 30 per minute - token mints: 5 per minute **Claimed Free** - concurrent sockets: 50 max at once - requests: 120 per minute - token mints: 30 per minute - token mints: 5000 per month - published messages: 500000 per month - history reads: 25000 per month **Claimed Plus** - concurrent sockets: 500 max at once - requests: 600 per minute - token mints: 150 per minute - token mints: 50000 per month - published messages: 5000000 per month - history reads: 250000 per month **Claimed Pro** - concurrent sockets: 2500 max at once - requests: 3000 per minute - token mints: 750 per minute - token mints: 250000 per month - published messages: 25000000 per month - history reads: 1250000 per month ### RealtimeKit media **Ephemeral** - participant tokens: 10 per ephemeral tenant lifetime before claim or expiry **Claimed Free** - No bucket cap is published for this tier; this surface is fluid-only after any tier-gating check. **Claimed Plus** - No bucket cap is published for this tier; this surface is fluid-only after any tier-gating check. **Claimed Pro** - No bucket cap is published for this tier; this surface is fluid-only after any tier-gating check. ### Notes - RealtimeKit media transport and claimed participant usage are not published hard buckets at launch. Ephemeral RealtimeKit participant-token issuance is capped to keep abandoned tenants bounded.