
convex-realtime
Builds reactive UIs on Convex subscriptions: useQuery patterns, optimistic updates, pagination that stays live, presence, and avoiding subscription churn. Use when wiring the frontend to Convex, when data does not update live, or when a page rerenders too much.
- Installs
- 2.9K
- Last 24h
- —
- Trending rank
- —
- Security
- Not audited
Install
Works with every agent the skills CLI supports.
npx skills add https://github.com/waynesutton/builder-skills --skill convex-realtime- 01
Install
Run the command in your project. The CLI asks which agents to install for.
- 02
Describe the task
Ask your agent for something this skill covers. It loads convex-realtime automatically when relevant.
- 03
Review the result
The agent follows the SKILL.md steps, scripts and references bundled with the skill.
SKILL.md
The instructions your agent reads when the skill is triggered.
Convex realtime
Every useQuery is a live subscription. The one rule: keep the set of documents a query reads small and indexed, because that read set decides when the query reruns.
When to reach for this
- Wiring a React component to Convex for the first time
- Data changes on the server but the page does not update
- A list or dashboard rerenders on every unrelated write
- A "load more" list drops or duplicates rows when new data lands
- Showing who is online or typing
How subscriptions work
useQuery opens a subscription over the client's websocket. The server runs the query, records its read set (every document and index range it touched), and pushes the result. When a mutation commits a write that overlaps that read set, the server reruns the query and pushes the new result. Nothing overlaps, nothing reruns. Results are cached per function and args, so two components calling useQuery(api.tasks.list, { userId }) share one subscription. All active subscriptions update together at the same database timestamp, so the UI never shows a half applied mutation. Queries must be deterministic for this to hold, which is why Date.now(), Math.random(), and fetch are not allowed inside them.
useQuery
undefined means loading. Pass "skip" instead of wrapping the hook in a condition.
import { useQuery } from "convex/react";
import { api } from "../convex/_generated/api";
import { Id } from "../convex/_generated/dataModel";
function TaskList({ userId }: { userId: Id<"users"> | null }) {
const tasks = useQuery(api.tasks.list, userId ? { userId } : "skip");
if (userId === null) return <p>Select a user</p>;
if (tasks === undefined) return <p>Loading</p>;
return (
<ul>
{tasks.map((task) => (
<li key={task._id}>{task.title}</li>
))}
</ul>
);
}
Backend queries return validated data and read through an index:
// convex/tasks.ts
import { query } from "./_generated/server";
import { v } from "convex/values";
export const list = query({
args: { userId: v.id("users") },
returns: v.array(
v.object({
_id: v.id("tasks"),
_creationTime: v.number(),
userId: v.id("users"),
title: v.string(),
completed: v.boolean(),
}),
),
handler: async (ctx, args) => {
return await ctx.db
.query("tasks")
.withIndex("by_user", (q) => q.eq("userId", args.userId))
.order("desc")
.take(100);
},
});
Mutations and optimistic updates
A plain useMutation call is enough for most UI. The subscription refreshes when the write commits, usually within a round trip. Add an optimistic update when that round trip is visible: toggles, reorders, chat sends.
import { useMutation } from "convex/react";
import { api } from "../convex/_generated/api";
import { Id } from "../convex/_generated/dataModel";
function useToggleTask(userId: Id<"users">) {
return useMutation(api.tasks.toggle).withOptimisticUpdate(
(localStore, args) => {
const current = localStore.getQuery(api.tasks.list, { userId });
if (current === undefined) return;
localStore.setQuery(
api.tasks.list,
{ userId },
current.map((task) =>
task._id === args.taskId
? { ...task, completed: !task.completed }
: task,
),
);
},
);
}
Inserting into a list works the same way. Build a temporary document with a placeholder _id and _creationTime: Date.now() (allowed here, this runs on the client), prepend it, and let the server result replace it. If the mutation throws, Convex rolls the local store back for you. Update every query the mutation affects, not just the one on screen, or the others will look stale until the server responds.
Pagination that stays live
// convex/messages.ts
import { query } from "./_generated/server";
import { v } from "convex/values";
import { paginationOptsValidator } from "convex/server";
export const listByChannel = query({
args: { channelId: v.id("channels"), paginationOpts: paginationOptsValidator },
// Return shape is fixed by .paginate(): { page, isDone, continueCursor }
handler: async (ctx, args) => {
return await ctx.db
.query("messages")
.withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
.order("desc")
.paginate(args.paginationOpts);
},
});
import { usePaginatedQuery } from "convex/react";
import { api } from "../convex/_generated/api";
import { Id } from "../convex/_generated/dataModel";
function MessageList({ channelId }: { channelId: Id<"channels"> }) {
const { results, status, loadMore } = usePaginatedQuery(
api.messages.listByChannel,
{ channelId },
{ initialNumItems: 30 },
);
return (
<div>
{results.map((m) => (
<p key={m._id}>{m.body}</p>
))}
{status === "CanLoadMore" && (
<button onClick={() => loadMore(30)}>Load more</button>
)}
{status === "LoadingMore" && <p>Loading</p>}
</div>
);
}
Each loaded page is its own subscription, so inserts and deletes anywhere in the list show up without refetching. status is LoadingFirstPage, CanLoadMore, LoadingMore, or Exhausted. For infinite scroll, call loadMore from an IntersectionObserver on a sentinel element when status === "CanLoadMore". Cursors come from .paginate() only. Do not build offset pagination by slicing a .collect() result.
Parallel loads with useQueries
Several useQuery calls in one component already load in parallel and stay consistent with each other. Reach for useQueries when the set of queries is dynamic, such as one query per id in a list.
import { useQueries } from "convex/react";
import { api } from "../convex/_generated/api";
import { Id } from "../convex/_generated/dataModel";
function Avatars({ userIds }: { userIds: Array<Id<"users">> }) {
const users = useQueries(
Object.fromEntries(
userIds.map((userId) => [userId, { query: api.users.get, args: { userId } }]),
),
);
return (
<div>
{userIds.map((id) => {
const user = users[id];
if (user === undefined) return <span key={id}>...</span>;
if (user instanceof Error) return <span key={id}>!</span>;
return <img key={id} src={user.avatarUrl} alt={user.name} />;
})}
</div>
);
}
Each value is undefined while loading, an Error on failure, or the query result.
Avoiding churn
Churn is a query rerunning for writes the component does not care about. Fix it on the server side first.
- Narrow the read set.
withIndexwith an equality or range reads one slice..filter()or a bare.query("table")reads the whole table, so any write to it reruns the query. - Return only what the component renders. A query that joins five tables reruns when any of the five change.
- Never
Date.now()in a query. Pass time as an argument and round it so the args stay stable:useQuery(api.tasks.overdue, { now: Math.floor(Date.now() / 60000) * 60000 })reruns once a minute, not on every render. - Bound results with
.take(n)or pagination..collect()on a growing table is a growing read set. - Split hot fields into their own table. A
lastSeentimestamp on the user document reruns every query that reads users. Put it in apresencetable with its own query. - Debounce mutations from rapid input (typing, dragging) so the subscription is not flooded with intermediate states.
Presence
Presence is a heartbeat mutation from each client plus a query over rows with a recent heartbeat, kept in a separate table so it does not touch anything else. Use the @convex-dev/presence component instead of building it. It handles heartbeats, disconnect cleanup, and a React hook.
// convex/convex.config.ts
import { defineApp } from "convex/server";
import presence from "@convex-dev/presence/convex.config";
const app = defineApp();
app.use(presence);
export default app;
Install with npm install @convex-dev/presence, then follow the package README for the server wrapper and the usePresence hook. If you must hand roll it, keep heartbeats in their own table, dedupe on the server with an early return when the last heartbeat is recent, and read with an index on roomId.
Common mistakes
| Mistake | Why it breaks | Do instead |
|---|---|---|
if (userId) useQuery(...) | hook order changes between renders | useQuery(fn, userId ? args : "skip") |
Treating undefined as empty | loading state renders as "no results" | check === undefined first |
Date.now() inside a query | nondeterministic, breaks caching | pass now as an arg, rounded |
.filter() on a large table | whole table in the read set | add an index, use withIndex |
.collect() for a feed | read set grows with the table | .take(n) or .paginate() |
| Optimistic update on one query only | other views stale until the server responds | update every affected query in the callback |
Offset pagination by slicing .collect() | pages drift as rows insert | .paginate() and usePaginatedQuery |
lastSeen on the user document | every user query reruns on each heartbeat | separate presence table or component |
Checklist
- Every
useQueryhandlesundefinedbefore reading the result - Conditional queries use
"skip", not conditional hook calls - Every backend query has
argsandreturnsvalidators - Every query reads through
withIndex, none use.filter()on a large table - No
Date.now()orMath.random()inside any query - Lists that can grow use
.paginate()andusePaginatedQuery - Optimistic updates touch every query the mutation changes
- Hot fields like heartbeats live in their own table
- Rapid input mutations are debounced on the client
Docs
FAQ
What does the convex-realtime skill do?
Builds reactive UIs on Convex subscriptions: useQuery patterns, optimistic updates, pagination that stays live, presence, and avoiding subscription churn. Use when wiring the frontend to Convex, when data does not update live, or when a page rerenders too much.
How do I install convex-realtime?
Run npx skills add https://github.com/waynesutton/builder-skills --skill convex-realtime in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/convex-realtime/ (personal) or .claude/skills/convex-realtime/ (project).
Which AI agents can use convex-realtime?
Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.
Is convex-realtime safe to install?
No partner security audit is available yet. Skills can run commands and scripts, so read the SKILL.md and any bundled files before installing.
Who maintains convex-realtime?
It is published by waynesutton in waynesutton/builder-skills. It has 2,877 recorded installs.
More from waynesutton/builder-skills
convex
waynesutton/builder-skills
Routes Convex work to the right builder skill and covers the handful of rules that apply everywhere: validators on every function, withIndex over filter, idempotent mutations, internal functions for scheduling. Use when a task mentions Convex and no more specific skill matches, or as the first stop before picking one.
convex-best-practices
waynesutton/builder-skills
Production patterns for Convex apps and the rules the @convex-dev/eslint-plugin enforces: validators, indexes, idempotent mutations, avoiding OCC conflicts, thin function wrappers, error handling. Use when reviewing Convex code, asking whether a pattern is right, setting up ESLint, or fixing write conflicts and slow queries.
convex-functions
waynesutton/builder-skills
Writes Convex queries, mutations, actions, and internal functions in the object form with args and returns validators, correct ctx usage, runtime boundaries, and error handling. Use when adding or changing anything in convex/.ts that exports a function, or when deciding between query, mutation, and action.
convex-schema-validator
waynesutton/builder-skills
Designs convex/schema.ts tables, validators, indexes, and relationships, and keeps the schema honest as data evolves. Use when creating tables, adding fields, choosing index fields, modeling relationships, or when a validator error appears at deploy time.
convex-cron-jobs
waynesutton/builder-skills
Schedules work in Convex: cron jobs in convex/crons.ts, one off scheduled functions with runAfter and runAt, batching large jobs, and cancelling or inspecting the queue. Use when something needs to run on a timer, later, or in the background, or when a cron is not firing.
convex-http-actions
waynesutton/builder-skills
Adds HTTP endpoints in convex/http.ts: webhook receivers with signature checks, REST style routes, CORS, auth headers, streaming responses, and file uploads over HTTP. Use when integrating Stripe, Clerk, Resend, or any service that calls back into the app, or when a client needs a plain HTTP API.
Popular frontend & design skills
setup-matt-pocock-skills
mattpocock/skills
Configure this repo for the engineering skills: set up its issue tracker, triage label vocabulary, and domain doc layout. Run once before first use of the other engineering skills.
frontend-design
anthropics/skills
Guidance for distinctive, intentional visual design when building new UI or reshaping an existing one. Helps with aesthetic direction, typography, and making choices that don't read as templated defaults.
hyperframes-animation
heygen-com/hyperframes
All animation knowledge for HyperFrames — atomic motion rules, multi-phase scene blueprints, scene transitions, broader motion-design techniques, AND the seven runtime adapters (GSAP default, plus Lottie, Three.js, Anime.js, CSS keyframes, Web Animations API, TypeGPU). Use for any motion or animation task: pick 2-4 rules and compose, or load a blueprint, or look up runtime-specific API (e.g. GSAP eases / Lottie player / Three.js mixer). Also covers auditing an existing composition's choreography (animation map) and 24 named text-animation effects. HyperFrames-native: single paused timeline, seek-safe, deterministic.
prototype
mattpocock/skills
Build a throwaway prototype to answer a design question. Use when the user wants to sanity-check whether a state model or logic feels right, or explore what a UI should look like.
hyperframes-core
heygen-com/hyperframes
The HyperFrames composition contract — build one renderable project. Use for composition structure, the data- timing attributes, class="clip", tracks, sub-compositions, variables, framework-owned media playback, deterministic-render rules, and validation. Read before writing composition HTML.
vercel-react-best-practices
vercel-labs/agent-skills
React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.