$npx -y skills add rivet-dev/skills --skill live-cursorsLive cursors and multiplayer presence with Rivet Actors: per-connection cursor state, realtime updates over events or raw WebSockets, and throttling.
| 1 | # Live Cursors and Presence |
| 2 | |
| 3 | **IMPORTANT: Before doing anything, you MUST read `BASE_SKILL.md` in this skill's directory. It contains essential guidance on debugging, error handling, state management, deployment, and project setup. Those rules and patterns apply to all RivetKit work. Everything below assumes you have already read and understood it.** |
| 4 | |
| 5 | ## Working Examples |
| 6 | |
| 7 | If you need a reference implementation, read the raw working example code in these templates: |
| 8 | |
| 9 | - [cursors](https://github.com/rivet-dev/rivet/tree/main/examples/cursors) |
| 10 | - [cursors-raw-websocket](https://github.com/rivet-dev/rivet/tree/main/examples/cursors-raw-websocket) |
| 11 | |
| 12 | |
| 13 | Patterns for building live cursors, multiplayer presence, and realtime cursor sharing with RivetKit. One room actor fans cursor positions out to every connected client, keyed per room with [actor keys](/docs/actors/keys). |
| 14 | |
| 15 | ## Starter Code |
| 16 | |
| 17 | Start with one of the two working variants on GitHub. Both implement the same collaborative cursor canvas with persistent text labels; they differ only in transport. |
| 18 | |
| 19 | | Variant | Starter Code | Transport | Presence Storage | |
| 20 | | --- | --- | --- | --- | |
| 21 | | `cursors` | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/cursors) | Typed [actions](/docs/actors/actions) and [events](/docs/actors/events) over the RivetKit connection | `connState` per connection | |
| 22 | | `cursors-raw-websocket` | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/cursors-raw-websocket) | Raw [`onWebSocket` handler](/docs/actors/websocket-handler) with a custom JSON message protocol | Socket map in `createVars` | |
| 23 | |
| 24 | Use `cursors` by default: typed actions, typed events, and automatic connection tracking cover most apps with less code. Use `cursors-raw-websocket` when you need full control of the wire format, for example a custom JSON or binary protocol, or clients that do not use the RivetKit client library. |
| 25 | |
| 26 | ## Connection State vs Persistent State |
| 27 | |
| 28 | Presence is ephemeral by definition. A cursor position is only meaningful while its connection is alive, so it belongs in per-connection storage, not in persistent actor state. Persistent state is reserved for data that must survive disconnects and actor restarts. |
| 29 | |
| 30 | | Data | Where It Lives | Why | |
| 31 | | --- | --- | --- | |
| 32 | | Cursor position | `connState` (`cursors`) or the `createVars` socket map (`cursors-raw-websocket`) | Scoped to one connection and discarded with it. Stale presence cannot accumulate in storage. | |
| 33 | | Text labels (`textLabels`) | Persistent actor `state` in both variants | Canvas content must survive disconnects and actor restarts. | |
| 34 | |
| 35 | In the `cursors` variant, `updateCursor` writes `c.conn.state.cursor` and `getRoomState` rebuilds the presence snapshot by iterating `c.conns.values()`, so the cursor map is always derived from live connections rather than stored. See [Connections](/docs/actors/connections) for `connState` and [State](/docs/actors/state) for persistence semantics. |
| 36 | |
| 37 | ## Presence Lifecycle |
| 38 | |
| 39 | - **Join**: The `cursors-raw-websocket` variant pushes an `init` message with the current `{ cursors, textLabels }` snapshot as soon as a socket connects. The `cursors` variant has no explicit join broadcast; the client calls the `getRoomState` action once after connecting to seed its local maps, and peers first see a new user on that user's first `cursorMoved` broadcast. |
| 40 | - **Move**: Every `updateCursor` call writes the connection's presence entry, then broadcasts `cursorMoved` to all connections, including the sender. |
| 41 | - **Leave**: The `cursors` variant handles leave in `onDisconnect`, broadcasting `cursorRemoved` with the connection's last cursor. The raw variant does the same from the socket `close` listener, then deletes the session from the `vars.websockets` map. Clients delete that user from their local cursor map, so stale cursors disappear the moment a tab closes. |
| 42 | |
| 43 | See [Lifecycle](/docs/actors/lifecycle) for `onDisconnect` and `createVars`. |
| 44 | |
| 45 | ## Update Throttling |
| 46 | |
| 47 | Neither example throttles. Both frontends send a cursor update on every raw `mousemove` event with no debounce or interval cap. That is fine for a demo, but a fast mouse on a high-refresh display can emit hundreds of events per second per user. The patterns below are recommended production hardening on top of the starter code, not something the examples implement. |
| 48 | |
| 49 | | Layer | Pattern | Guidance | |
| 50 | | --- | --- | --- | |
| 51 | | Client (smoothness) | Throttle to 20-30Hz | Sample the latest pointer position every 33-50ms and send only that. Drop intermediate moves, but always flush the final position so cursors settle at the true location. Interpolate between received positions on the rendering side. | |
| 52 | | Server (enforcement) | Per-connection rate limit | Track the last accepted update timestamp per connection and drop or coalesce updates arriving f |