convex-realtime
Builds reactive features with Convex real-time patterns.
Install
mkdir -p .claude/skills/convex-realtime && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/3703" && unzip -o skill.zip -d .claude/skills/convex-realtime && rm skill.zipInstalls to .claude/skills/convex-realtime
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
Patterns for building reactive apps including subscription management, optimistic updates, cache behavior, and paginated queries with cursor-based loadingKey capabilities
- →Implement automatic query subscription patterns
- →Manage optimistic update logic for UI consistency
- →Configure cursor-based pagination for large datasets
- →Generate conditional query blocks to avoid unnecessary network calls
How it works
It applies established patterns for Convex hooks and cache management to ensure reactive state synchronization between the database and the UI.
Inputs & outputs
When to use convex-realtime
- →Implementing live data dashboards
- →Adding optimistic UI updates
- →Building cursor-based pagination
About this skill
Convex Realtime
Build reactive applications with Convex's real-time subscriptions, optimistic updates, intelligent caching, and cursor-based pagination.
Documentation Sources
Before implementing, do not assume; fetch the latest documentation:
- Primary: https://docs.convex.dev/client/react
- Optimistic Updates: https://docs.convex.dev/client/react/optimistic-updates
- Pagination: https://docs.convex.dev/database/pagination
- For broader context: https://docs.convex.dev/llms.txt
Instructions
How Convex Realtime Works
- Automatic Subscriptions - useQuery creates a subscription that updates automatically
- Smart Caching - Query results are cached and shared across components
- Consistency - All subscriptions see a consistent view of the database
- Efficient Updates - Only re-renders when relevant data changes
Basic Subscriptions
// React component with real-time data
import { useQuery } from "convex/react";
import { api } from "../convex/_generated/api";
function TaskList({ userId }: { userId: Id<"users"> }) {
// Automatically subscribes and updates in real-time
const tasks = useQuery(api.tasks.list, { userId });
if (tasks === undefined) {
return <div>Loading...</div>;
}
return (
<ul>
{tasks.map((task) => (
<li key={task._id}>{task.title}</li>
))}
</ul>
);
}
Conditional Queries
import { useQuery } from "convex/react";
import { api } from "../convex/_generated/api";
function UserProfile({ userId }: { userId: Id<"users"> | null }) {
// Skip query when userId is null
const user = useQuery(
api.users.get,
userId ? { userId } : "skip"
);
if (userId === null) {
return <div>Select a user</div>;
}
if (user === undefined) {
return <div>Loading...</div>;
}
return <div>{user.name}</div>;
}
Mutations with Real-time Updates
import { useMutation, useQuery } from "convex/react";
import { api } from "../convex/_generated/api";
function TaskManager({ userId }: { userId: Id<"users"> }) {
const tasks = useQuery(api.tasks.list, { userId });
const createTask = useMutation(api.tasks.create);
const toggleTask = useMutation(api.tasks.toggle);
const handleCreate = async (title: string) => {
// Mutation triggers automatic re-render when data changes
await createTask({ title, userId });
};
const handleToggle = async (taskId: Id<"tasks">) => {
await toggleTask({ taskId });
};
return (
<div>
<button onClick={() => handleCreate("New Task")}>Add Task</button>
<ul>
{tasks?.map((task) => (
<li key={task._id} onClick={() => handleToggle(task._id)}>
{task.completed ? "✓" : "○"} {task.title}
</li>
))}
</ul>
</div>
);
}
Optimistic Updates
Show changes immediately before server confirmation:
import { useMutation, useQuery } from "convex/react";
import { api } from "../convex/_generated/api";
import { Id } from "../convex/_generated/dataModel";
function TaskItem({ task }: { task: Task }) {
const toggleTask = useMutation(api.tasks.toggle).withOptimisticUpdate(
(localStore, args) => {
const { taskId } = args;
const currentValue = localStore.getQuery(api.tasks.get, { taskId });
if (currentValue !== undefined) {
localStore.setQuery(api.tasks.get, { taskId }, {
...currentValue,
completed: !currentValue.completed,
});
}
}
);
return (
<div onClick={() => toggleTask({ taskId: task._id })}>
{task.completed ? "✓" : "○"} {task.title}
</div>
);
}
Optimistic Updates for Lists
import { useMutation } from "convex/react";
import { api } from "../convex/_generated/api";
function useCreateTask(userId: Id<"users">) {
return useMutation(api.tasks.create).withOptimisticUpdate(
(localStore, args) => {
const { title, userId } = args;
const currentTasks = localStore.getQuery(api.tasks.list, { userId });
if (currentTasks !== undefined) {
// Add optimistic task to the list
const optimisticTask = {
_id: crypto.randomUUID() as Id<"tasks">,
_creationTime: Date.now(),
title,
userId,
completed: false,
};
localStore.setQuery(api.tasks.list, { userId }, [
optimisticTask,
...currentTasks,
]);
}
}
);
}
Cursor-Based Pagination
// convex/messages.ts
import { query } from "./_generated/server";
import { v } from "convex/values";
import { paginationOptsValidator } from "convex/server";
export const listPaginated = query({
args: {
channelId: v.id("channels"),
paginationOpts: paginationOptsValidator,
},
handler: async (ctx, args) => {
return await ctx.db
.query("messages")
.withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
.order("desc")
.paginate(args.paginationOpts);
},
});
// React component with pagination
import { usePaginatedQuery } from "convex/react";
import { api } from "../convex/_generated/api";
function MessageList({ channelId }: { channelId: Id<"channels"> }) {
const { results, status, loadMore } = usePaginatedQuery(
api.messages.listPaginated,
{ channelId },
{ initialNumItems: 20 }
);
return (
<div>
{results.map((message) => (
<div key={message._id}>{message.content}</div>
))}
{status === "CanLoadMore" && (
<button onClick={() => loadMore(20)}>Load More</button>
)}
{status === "LoadingMore" && <div>Loading...</div>}
{status === "Exhausted" && <div>No more messages</div>}
</div>
);
}
Infinite Scroll Pattern
import { usePaginatedQuery } from "convex/react";
import { useEffect, useRef } from "react";
import { api } from "../convex/_generated/api";
function InfiniteMessageList({ channelId }: { channelId: Id<"channels"> }) {
const { results, status, loadMore } = usePaginatedQuery(
api.messages.listPaginated,
{ channelId },
{ initialNumItems: 20 }
);
const observerRef = useRef<IntersectionObserver>();
const loadMoreRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (observerRef.current) {
observerRef.current.disconnect();
}
observerRef.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && status === "CanLoadMore") {
loadMore(20);
}
});
if (loadMoreRef.current) {
observerRef.current.observe(loadMoreRef.current);
}
return () => observerRef.current?.disconnect();
}, [status, loadMore]);
return (
<div>
{results.map((message) => (
<div key={message._id}>{message.content}</div>
))}
<div ref={loadMoreRef} style={{ height: 1 }} />
{status === "LoadingMore" && <div>Loading...</div>}
</div>
);
}
Multiple Subscriptions
import { useQuery } from "convex/react";
import { api } from "../convex/_generated/api";
function Dashboard({ userId }: { userId: Id<"users"> }) {
// Multiple subscriptions update independently
const user = useQuery(api.users.get, { userId });
const tasks = useQuery(api.tasks.list, { userId });
const notifications = useQuery(api.notifications.unread, { userId });
const isLoading = user === undefined ||
tasks === undefined ||
notifications === undefined;
if (isLoading) {
return <div>Loading...</div>;
}
return (
<div>
<h1>Welcome, {user.name}</h1>
<p>You have {tasks.length} tasks</p>
<p>{notifications.length} unread notifications</p>
</div>
);
}
Examples
Real-time Chat Application
// convex/messages.ts
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
export const list = query({
args: { channelId: v.id("channels") },
returns: v.array(v.object({
_id: v.id("messages"),
_creationTime: v.number(),
content: v.string(),
authorId: v.id("users"),
authorName: v.string(),
})),
handler: async (ctx, args) => {
const messages = await ctx.db
.query("messages")
.withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
.order("desc")
.take(100);
// Enrich with author names
return Promise.all(
messages.map(async (msg) => {
const author = await ctx.db.get(msg.authorId);
return {
...msg,
authorName: author?.name ?? "Unknown",
};
})
);
},
});
export const send = mutation({
args: {
channelId: v.id("channels"),
authorId: v.id("users"),
content: v.string(),
},
returns: v.id("messages"),
handler: async (ctx, args) => {
return await ctx.db.insert("messages", {
channelId: args.channelId,
authorId: args.authorId,
content: args.content,
});
},
});
// ChatRoom.tsx
import { useQuery, useMutation } from "convex/react";
import { api } from "../convex/_generated/api";
import { useState, useRef, useEffect } from "react";
function ChatRoom({ channelId, userId }: Props) {
const messages = useQuery(api.messages.list, { channelId });
const sendMessage = useMutation(api.messages.send);
const [input, setInput] = useState("");
const messagesEndRef = useRef<HTMLDivElement>(null);
// Auto-scroll to bottom on new messages
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
const handleSend = async (e: React.FormEvent) => {
e.preventDefault();
if (!input.trim()) return;
await sendMessage({
channelId,
authorId: userId,
content: input.trim(),
});
setInput("");
};
return (
<div className="chat-room">
<div className="messages">
---
*Content truncated.*
When not to use it
- →Simple static data fetching
- →Backend-heavy logic that does not involve UI reactivity
Limitations
- →Depends on Convex React client conventions
- →Requires knowledge of Convex query return types
How it compares
It provides ready-to-use patterns for data synchronization and optimistic updates, which reduces manual state-management overhead.
Compared to similar skills
convex-realtime side by side with the closest alternatives in the catalog.
| Skill | Installs | Updated | Safety | Difficulty |
|---|---|---|---|---|
| convex-realtime (this skill) | 1 | 6mo | No flags | Intermediate |
| lifi-dev | 0 | 6mo | Review | Advanced |
| api | 0 | 6mo | No flags | Advanced |
| hr-portal-skill | 0 | 1mo | No flags | Beginner |
Try saying
Example prompts that trigger this skill in your AI assistant.
More by waynesutton
View all by waynesutton →You might also like
lifi-dev
mashharuki
>
api
birdgg
Create an agent team for full-stack feature development. One agent writes the Haskell Servant backend API, another builds the React frontend UI consuming that API. Use when asked to "build a feature", "add a new page", "create an endpoint with UI", or "fullstack feature".
hr-portal-skill
hariventures2000-ship-it
Use this skill when developing, testing, debugging, or modifying code inside the HR Portal frontend (apps/hr-portal). It applies to handling HR administration tasks such as employee roster management, contract seeding, leave request approvals, attendance monitoring, payroll calculation, and recruitm
fullstack-guardian
Jeffallan
Use when implementing features across frontend and backend, building APIs with UI, or creating end-to-end data flows. Invoke for feature implementation, API development, UI building, cross-stack work.
javascript-typescript-typescript-scaffold
sickn33
You are a TypeScript project architecture expert specializing in scaffolding production-ready Node.js and frontend applications. Generate complete project structures with modern tooling (pnpm, Vite, N
wagmi-development
wevm
Creates Wagmi features across all layers - core actions, query options, framework bindings. Use when adding new actions, hooks, or working across packages/core, packages/react, packages/vue.