> ## Documentation Index
> Fetch the complete documentation index at: https://vlyai-1c28d863.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Real-time sync patterns

> Patterns for sync, presence, collaborative editing, and other live-updating features.

Convex makes basic real-time sync free — every `useQuery` is a subscription. Some features need more than basic sync: presence, optimistic updates, collaborative editing. The patterns:

## Pattern: presence

"Show who's currently viewing this document."

```typescript convex/schema.ts theme={null}
presence: defineTable({
  documentId: v.id("documents"),
  userId:     v.id("users"),
  lastSeen:   v.number(),
}).index("by_document", ["documentId"])
```

```typescript convex/presence.ts theme={null}
export const heartbeat = mutation({
  args: { documentId: v.id("documents") },
  handler: async (ctx, { documentId }) => {
    const u = await ctx.auth.getUserIdentity();
    if (!u) return;
    const existing = await ctx.db
      .query("presence")
      .withIndex("by_document", q => q.eq("documentId", documentId))
      .filter(q => q.eq(q.field("userId"), u.subject))
      .unique();
    if (existing) {
      await ctx.db.patch(existing._id, { lastSeen: Date.now() });
    } else {
      await ctx.db.insert("presence", {
        documentId, userId: u.subject as Id<"users">, lastSeen: Date.now(),
      });
    }
  },
});

export const list = query({
  args: { documentId: v.id("documents") },
  handler: async (ctx, { documentId }) => {
    const cutoff = Date.now() - 30_000;
    const all = await ctx.db.query("presence")
      .withIndex("by_document", q => q.eq("documentId", documentId))
      .collect();
    return all.filter(p => p.lastSeen > cutoff);
  },
});
```

Client calls `heartbeat` every 10 seconds; `list` shows who's active in the last 30s.

A scheduled function cleans up stale rows nightly.

## Pattern: collaborative editing

Two approaches:

<CardGroup cols={2}>
  <Card title="Operational transformation (OT)" icon="git-merge">
    Each edit is a transform. Server reconciles concurrent edits. Complex; use a library (Yjs, Automerge).
  </Card>

  <Card title="Last-write-wins per field" icon="clock">
    Each field is updated independently; last write wins. Simple; works for forms and structured docs.
  </Card>
</CardGroup>

For most apps, the second approach is enough. For Google-Docs-class collaborative text editing, integrate Yjs with a Convex sync provider.

## Pattern: live cursors

Same shape as presence, but adds `cursor: { x, y }` updated on mousemove (debounced to 30 fps).

## Pattern: notifications stream

A `notifications` table indexed `by_user_and_read`. Component subscribes via `useQuery` for the unread set. Reactive sync delivers new notifications instantly.

## Limits

* \~10,000 concurrent subscribers per query (soft).
* Per-document presence with 1000+ users requires sharding or a different store.

## Tips

<Tip>
  **Don't store cursor positions in your main table.** Put them in a separate `cursors` table; cleanup is much easier.
</Tip>

<Tip>
  **Start without OT.** Most "collaboration" features (comments, presence, simultaneous form editing on different fields) work fine without OT.
</Tip>

## Related

<CardGroup cols={3}>
  <Card title="Real-time sync" icon="zap" href="/features/data/realtime-sync">
    The basics.
  </Card>

  <Card title="Real-time chat recipe" icon="message-square-text" href="/recipes/realtime-chat-app">
    A worked example.
  </Card>

  <Card title="Multiplayer game recipe" icon="gamepad-2" href="/recipes/multiplayer-game">
    Heavier real-time use.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.