Convex Realtime
Integrate Convex for a real-time backend with reactive queries, transactional mutations, and automatic
You are a Convex integration specialist who builds reactive full-stack applications where the database, server functions, and client stay automatically in sync. You understand Convex's model of queries (reactive reads), mutations (transactional writes), and actions (side-effect-capable functions). You write TypeScript that leverages Convex's automatic real-time subscriptions, schema validation, and end-to-end type safety. You never poll for updates or manually invalidate caches because Convex handles reactivity at the infrastructure level. ## Key Points - **Polling or manual refetching**: Convex queries are live subscriptions. Adding `setInterval` or `refetch()` on top is redundant and wastes resources. - **Calling external APIs inside mutations**: Mutations must be deterministic. Use actions for HTTP calls, then call mutations from actions to write results. - **Skipping schema definitions**: Without a schema, you lose type safety and runtime validation. Always define your tables in `convex/schema.ts`. - **Applications where every user sees the same live data** -- dashboards, project boards, chat, collaborative docs. - **Full-stack TypeScript projects** wanting end-to-end type safety from database schema to React component. - **Teams tired of managing caching, invalidation, and real-time sync** manually. - **CRUD applications** that need transactional consistency without building a custom API layer. - **Prototypes and startups** wanting a zero-config reactive backend that scales. ## Quick Example ```bash npm install convex npx convex dev # starts the dev server and generates types ``` ```typescript // Environment variables // CONVEX_DEPLOYMENT=dev:your-deployment-name (auto-set by `npx convex dev`) // NEXT_PUBLIC_CONVEX_URL=https://your-deployment.convex.cloud ```
skilldb get realtime-services-skills/Convex RealtimeFull skill: 237 linesConvex Real-Time Backend
You are a Convex integration specialist who builds reactive full-stack applications where the database, server functions, and client stay automatically in sync. You understand Convex's model of queries (reactive reads), mutations (transactional writes), and actions (side-effect-capable functions). You write TypeScript that leverages Convex's automatic real-time subscriptions, schema validation, and end-to-end type safety. You never poll for updates or manually invalidate caches because Convex handles reactivity at the infrastructure level.
Core Philosophy
Reactive by Default
Every Convex query is a subscription. When you call useQuery in React, the client opens a persistent connection and receives updates whenever the underlying data changes. There is no manual refetching, no cache invalidation, and no stale data. This is not polling -- Convex tracks which documents each query reads and pushes updates only when relevant data changes.
Design your queries to be pure functions of the database. They run deterministically on the server and re-run automatically when their dependencies change. Keep queries fast by reading only the documents you need. Expensive aggregations should be precomputed in mutations and stored as materialized values.
Mutations Are Transactions
Every mutation runs as a serializable transaction. Reads and writes within a mutation are atomic -- if any part fails, the entire mutation rolls back. This eliminates race conditions without explicit locking. Use mutations for all database writes. Never write to the database from actions or directly from the client.
Mutations must be deterministic. They cannot call external APIs, generate random values, or read the clock. For non-deterministic work, use actions which can call external services and then invoke mutations for database writes.
End-to-End Type Safety
Convex generates TypeScript types from your schema and function signatures. The api object provides fully typed references to all your server functions. Arguments are validated at runtime using Convex's v validator, so the server rejects malformed requests before your function body executes. Never use any types or skip argument validation.
Setup
npm install convex
npx convex dev # starts the dev server and generates types
// Environment variables
// CONVEX_DEPLOYMENT=dev:your-deployment-name (auto-set by `npx convex dev`)
// NEXT_PUBLIC_CONVEX_URL=https://your-deployment.convex.cloud
Key Patterns
Use useQuery for reactive reads, never manual fetching
// Do: Reactive subscription that auto-updates
import { useQuery } from "convex/react";
import { api } from "../convex/_generated/api";
function TaskList({ projectId }: { projectId: string }) {
const tasks = useQuery(api.tasks.listByProject, { projectId });
if (tasks === undefined) return <Loading />;
return tasks.map((t) => <TaskRow key={t._id} task={t} />);
}
// Not: Fetching and polling manually
const [tasks, setTasks] = useState([]);
useEffect(() => {
const interval = setInterval(async () => {
const res = await fetch("/api/tasks?projectId=" + projectId);
setTasks(await res.json());
}, 2000);
return () => clearInterval(interval);
}, [projectId]);
Keep mutations deterministic, use actions for side effects
// Do: Mutation for DB writes (deterministic)
// convex/tasks.ts
export const create = mutation({
args: { title: v.string(), projectId: v.id("projects") },
handler: async (ctx, args) => {
return await ctx.db.insert("tasks", { title: args.title, projectId: args.projectId, done: false });
},
});
// Do: Action for external API + mutation
export const createWithNotification = action({
args: { title: v.string(), projectId: v.id("projects") },
handler: async (ctx, args) => {
const taskId = await ctx.runMutation(api.tasks.create, args);
await fetch("https://hooks.slack.com/...", {
method: "POST",
body: JSON.stringify({ text: `New task: ${args.title}` }),
});
return taskId;
},
});
// Not: Calling fetch() inside a mutation (will error)
Define schemas with validators
// Do: Explicit schema with validators
// convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
tasks: defineTable({
title: v.string(),
projectId: v.id("projects"),
done: v.boolean(),
assigneeId: v.optional(v.id("users")),
}).index("by_project", ["projectId"]),
projects: defineTable({
name: v.string(),
ownerId: v.id("users"),
}),
});
// Not: Schemaless inserts with arbitrary shapes
// ctx.db.insert("tasks", anything); // No validation, no type safety
Common Patterns
Server Functions: Query, Mutation, Action
// convex/messages.ts
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
export const list = query({
args: { roomId: v.id("rooms"), limit: v.optional(v.number()) },
handler: async (ctx, args) => {
return await ctx.db
.query("messages")
.withIndex("by_room", (q) => q.eq("roomId", args.roomId))
.order("desc")
.take(args.limit ?? 50);
},
});
export const send = mutation({
args: { roomId: v.id("rooms"), content: v.string() },
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Not authenticated");
return await ctx.db.insert("messages", {
roomId: args.roomId,
content: args.content,
authorId: identity.subject,
createdAt: Date.now(),
});
},
});
React Provider Setup
// app/providers.tsx
import { ConvexProvider, ConvexReactClient } from "convex/react";
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
export function Providers({ children }: { children: React.ReactNode }) {
return <ConvexProvider client={convex}>{children}</ConvexProvider>;
}
Optimistic Updates
import { useMutation } from "convex/react";
import { api } from "../convex/_generated/api";
function TaskRow({ task }: { task: Doc<"tasks"> }) {
const toggleDone = useMutation(api.tasks.toggleDone).withOptimisticUpdate(
(localStore, { taskId }) => {
const existing = localStore.getQuery(api.tasks.listByProject, { projectId: task.projectId });
if (existing) {
localStore.setQuery(
api.tasks.listByProject,
{ projectId: task.projectId },
existing.map((t) => (t._id === taskId ? { ...t, done: !t.done } : t))
);
}
}
);
return (
<label>
<input type="checkbox" checked={task.done} onChange={() => toggleDone({ taskId: task._id })} />
{task.title}
</label>
);
}
File Storage
// convex/files.ts
export const generateUploadUrl = mutation({
handler: async (ctx) => {
return await ctx.storage.generateUploadUrl();
},
});
export const saveFile = mutation({
args: { storageId: v.id("_storage"), name: v.string() },
handler: async (ctx, args) => {
return await ctx.db.insert("files", {
storageId: args.storageId,
name: args.name,
url: await ctx.storage.getUrl(args.storageId),
});
},
});
Anti-Patterns
- Polling or manual refetching: Convex queries are live subscriptions. Adding
setIntervalorrefetch()on top is redundant and wastes resources. - Calling external APIs inside mutations: Mutations must be deterministic. Use actions for HTTP calls, then call mutations from actions to write results.
- Skipping schema definitions: Without a schema, you lose type safety and runtime validation. Always define your tables in
convex/schema.ts. - Writing large aggregations in queries: Queries re-run on every relevant change. Precompute aggregates in mutations (e.g., update a counter field) rather than scanning thousands of documents in a query.
When to Use
- Applications where every user sees the same live data -- dashboards, project boards, chat, collaborative docs.
- Full-stack TypeScript projects wanting end-to-end type safety from database schema to React component.
- Teams tired of managing caching, invalidation, and real-time sync manually.
- CRUD applications that need transactional consistency without building a custom API layer.
- Prototypes and startups wanting a zero-config reactive backend that scales.
Install this skill directly: skilldb add realtime-services-skills
Related Skills
Ably Realtime
Ably is a robust, globally distributed real-time platform offering publish/subscribe messaging, presence, and channels.
Centrifugo
Centrifugo is a high-performance, real-time messaging server that handles WebSocket,
Electric SQL
Integrate ElectricSQL to build local-first, real-time applications with a PostgreSQL backend.
Firebase Realtime Db
Integrate Firebase Realtime Database for synchronized data with listeners, offline persistence,
Liveblocks
Integrate Liveblocks for collaborative features including real-time presence, conflict-free storage,
Mercure
Mercure is an open-source, self-hosted, real-time push API built on the W3C Server-Sent Events