Drop-in Figma-style multiplayer cursors for Convex apps with room presence, heartbeat/TTL, batched positions, and a React overlay with interpolated playback.
npm install convex-multiplayer-cursorsconvex-multiplayer-cursors is a Convex component that provides Figma-style real-time cursor presence across rooms. It splits state into three tables (sessions, beats, positions) with distinct invalidation profiles so heartbeats trigger zero query re-runs and cursor movement invalidates only a single hot query. The React overlay drives DOM updates through a requestAnimationFrame loop with interpolated playback, producing zero React re-renders per cursor movement.
convex-multiplayer-cursors mounts as a Convex component via app.use in convex.config.ts, exposing a Cursors class you wrap with your own authenticated mutations and queries. On the frontend, CursorsOverlay from convex-multiplayer-cursors/react renders an interpolated cursor overlay inside any position:relative container. The component handles room lifecycle, session expiry, and batched position flushing automatically.
convex-multiplayer-cursors implements the same pattern as Figma's collaborative cursors: remote cursors lag by a configurable lagMs (default 260ms) so the client always has a next sample to interpolate toward, producing smooth gliding instead of teleporting. Positions are batched and flushed at most every 200ms while the pointer moves, and only while the user has an active session.
The component separates data into three tables with distinct invalidation profiles: sessions (invalidated on join/leave only), beats (never subscribed to, so heartbeats trigger zero query re-executions), and positions (subscribed only by the hot positions query). This means roster re-renders happen only on join or leave, and cursor movement touches only one narrow subscription.
convex-multiplayer-cursors manages presence through a beat mutation that records a heartbeat timestamp with a 45-second TTL by default. Cleanup is handled by a scheduled expiry per beat plus opportunistic pruning by live room participants. On the client, the heartbeat skips hidden tabs and the session is automatically reborn on focus, preventing flapping.
No. convex-multiplayer-cursors never reads ctx.auth directly. You write thin wrapper mutations and queries in your own convex/ directory that authenticate via ctx.auth, determine the room key server-side, and pass userId into the component's Cursors class methods. This follows the standard Convex component pattern where the component receives only validated, app-supplied values.
The default room capacity is 128 sessions. When a room is full, the beat mutation returns false instead of creating a new session. The client-side hook detects a refused beat and automatically re-keys the sessionId, so a returning user whose session expired and was reclaimed can still rejoin without manual intervention.
convex-multiplayer-cursors uses single-flight batching: at most one mutation is in flight at a time, and position samples are flushed at most once every 200ms and only while the pointer is actively moving. On the subscriber side, movement invalidates only the positions query, not the session roster. Cursor positions are applied to the DOM in a requestAnimationFrame loop without triggering React re-renders.
Yes. The package exports a useCursors hook plus pure utility functions for timeline interpolation and sample batching from convex-multiplayer-cursors/react. You can use these primitives to build a fully custom rendering layer while still relying on the component's backend for room management and data transport.
Yes, convex-multiplayer-cursors ships plain TypeScript source without a separate build step. On Next.js you need to add convex-multiplayer-cursors to the transpilePackages array in your Next.js config so the framework processes it through its own build pipeline. Vite and standard tsc with moduleResolution set to bundler work without any extra configuration.