# Realtime Real-time messaging with channels, presence, and runtime-selected managed persistence. WebSocket handles live delivery; HTTP handles server-side publish and history. No third-party service is needed. ## Prerequisites Edge calls require this resource to be provisioned first; unprovisioned calls return an 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 " Provisioning happens once, before the application runs; the running application does not provision its own resources. ## 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"`). Minting realtime tokens or comparing presence data against a social-login ID without casting to `String(user.id)` produces a silent type mismatch. - **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 - Fresh realtime provisioning on a fresh capability runtime stores persisted history in the private `coh_realtime.realtime_messages` schema of the shared tenant Neon project. This does not provision D1 or grant `/edge/postgres`. - Existing realtime resources and missing backend state remain legacy D1. Runtime upgrades never move, backfill, or delete their data; there is no migration endpoint in this release. - History and publication shapes, ordering, cursor IDs, and numeric `created_at_ms` values remain identical on both backends. - Video/voice rooms are lazily provisioned on first room creation (powered by Cloudflare RealtimeKit): no extra setup needed - Deleting realtime drops only its configured history store and any RealtimeKit app. It never deletes an existing tenant D1; a shared Neon project is reclaimed only when realtime was its last dependent. You can choose the managed history 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"}' `write_region` remains a compatibility input. It selects the write region for a legacy D1 backend and maps to the nearest supported Neon region for a fresh capability backend. Direct D1 regions (`wnam`, `enam`, `weur`, `eeur`, `apac`, `oc`) and aliases such as `us-west`, `us`, `eu`, `eu-east`, `apac`, and `australia` remain accepted; omitting it uses the backend default. > **Server-side only.** `coh_application_key` is a secret; browser JS, mobile bundles, and other client-side code cannot hold it safely. This call belongs in a Railway-hosted server, `cloudflare-workers`, or your own server tier. 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 the configured managed history store 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 Minting a short-lived edge bearer token from your server first speeds up repeated edge calls: 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`. The WebSocket connection token can then be minted using that bearer token (recommended), or via 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). Chat-style apps where users stay on a page for hours benefit from a longer `ttl`, such as `1800` or `3600`. Existing WebSocket connections do not auto-refresh, so reconnecting after expiry requires a fresh token. The Worker re-checks that the tenant and `realtime` resource are still active when the WebSocket opens. This endpoint runs server-side only: the application key it uses lives on the server, and exposing that key to the browser would let any client mint tokens directly. The resulting token is what gets passed to the client. ## Connect (WebSocket) Each WebSocket connects to exactly one channel: 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. Watching another live channel requires a separate WebSocket. ## Publish via WebSocket Publishing sends a frame 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 } Setting `"persist": false` skips managed persistence, 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: realtime user IDs are always strings. Comparing them against numeric auth IDs (e.g., social-login's `id: 25`) without casting to `String(user.id)` causes a mismatch. ## Publish (HTTP) HTTP publishing works from server-side code (Railway services, CF Workers, cron jobs), without needing 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 }` Setting `"persist": false` in the body skips managed persistence. ## History Persisted messages for a channel can be fetched from the configured managed history store, for scrollback or 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 and use the primary configured history store. 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) Legacy D1-backed realtime tenants keep their existing direct `realtime_messages` table access through the existing-tenants-only `database` offering. Fresh capability tenants use the backend-independent history endpoint above; the reserved `coh_realtime` schema is not exposed through `/edge/postgres`. ## 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 - Disconnecting or reconnecting requires a **fresh token**, since the old one may have expired. A longer validity window (up to 3600s) is available via `ttl`. - `GET /edge/realtime/history?after=` catches 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:** `dm:_` with sorted IDs lets 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, a notification goes 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: once `realtime` is provisioned, 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}/`, with account ID and app ID injected automatically. Responses follow Cloudflare's `{ success, data: {...} }` envelope format. For participant creation, `data.token` from the response is the participant auth token. ### Eventual consistency after meeting creation Meeting creation is eventually consistent: calling participant endpoints right after creation, with the freshly-returned `id`, may briefly return `ResourceNotFound`. Polling `GET /edge/realtimekit/meetings/:id` until it resolves, before minting participants, avoids the race. A loop of 5 attempts at 200ms apart is sufficient. On the frontend, the Cloudflare RealtimeKit SDK takes 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 ## 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, AI Gateway, Deepgram, and Exa are fluid-only after tier, rate, and concurrency checks; AI Gateway and Deepgram have 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.