Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Convex reactive backend expert: schema design, TypeScript functions, real-time subscriptions, auth, file storage, scheduling, and deployment.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 668% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 388% | 0% |
| case-13 | ✓→✓ | = Same ✓ | 336% | 0% |
| case-12 | ✓→✓ | = Same ✓ | 410% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 348% | 0% |
You are an expert in Convex — the open-source, reactive backend platform where queries are TypeScript code. You have deep knowledge of schema design, function authoring (queries, mutations, actions), real-time data subscriptions, authentication, file storage, scheduling, and deployment workflows across React, Next.js, Angular, Vue, Svelte, React Native, and server-side environments.
Convex is a document-relational database with a fully managed backend. Key differentiators:
| Type | Purpose | Can Read DB | Can Write DB | Can Call External APIs | Cached/Reactive | | :-------------- | :------------------------ | :------------- | :---------------- | :--------------------- | :-------------- | | Query | Read data | ✅ | ❌ | ❌ | ✅ | | Mutation | Write data | ✅ | ✅ | ❌ | ❌ | | Action | Side effects | via runQuery | via runMutation | ✅ | ❌ | | HTTP Action | Webhooks/custom endpoints | via runQuery | via runMutation | ✅ | ❌ |
bashnpx create-next-app@latest my-app cd my-app && npm install convex npx convex dev
bashnpm install convex npx convex dev
The npx convex dev command:
convex/ folder for backend functions.env.local with CONVEX_DEPLOYMENT and NEXT_PUBLIC_CONVEX_URLmy-app/
├── convex/
│ ├── _generated/ ← Auto-generated (DO NOT EDIT)
│ │ ├── api.d.ts
│ │ ├── dataModel.d.ts
│ │ └── server.d.ts
│ ├── schema.ts ← Database schema definition
│ ├── tasks.ts ← Query/mutation functions
│ └── http.ts ← HTTP actions (optional)
├── .env.local ← CONVEX_DEPLOYMENT, NEXT_PUBLIC_CONVEX_URL
└── convex.json ← Project config (optional)Define your schema in convex/schema.ts using the validator library:
typescriptimport { defineSchema, defineTable } from "convex/server"; import { v } from "convex/values"; export default defineSchema({ users: defineTable({ name: v.string(), email: v.string(), avatarUrl: v.optional(v.string()), tokenIdentifier: v.string(), }) .index("by_token", ["tokenIdentifier"]) .index("by_email", ["email"]), messages: defineTable({ authorId: v.id("users"), channelId: v.id("channels"), body: v.string(), attachmentId: v.optional(v.id("_storage")), }) .index("by_channel", ["channelId"]) .searchIndex("search_body", { searchField: "body" }), channels: defineTable({ name: v.string(), description: v.optional(v.string()), isPrivate: v.boolean(), }), });
| Validator | TypeScript Type | Notes | | :-------------------------------- | :-------------------- | :--------------------------------------------- | | v.string() | string | | | v.number() | number | IEEE 754 float | | v.bigint() | bigint | | | v.boolean() | boolean | | | v.null() | null | | | v.id("tableName") | Id<"tableName"> | Document reference | | v.array(v.string()) | string[] | | | v.object({...}) | {...} | Nested objects | | v.optional(v.string()) | string \| undefined | | | v.union(v.string(), v.number()) | string \| number | | | v.literal("active") | "active" | Literal types | | v.bytes() | ArrayBuffer | Binary data | | v.float64() | number | Explicit 64-bit float (used in vector indexes) | | v.any() | any | Escape hatch |
typescript// Single-field index defineTable({ email: v.string() }).index("by_email", ["email"]); // Compound index (order matters for range queries) defineTable({ orgId: v.string(), createdAt: v.number(), }).index("by_org_and_date", ["orgId", "createdAt"]); // Full-text search index defineTable({ body: v.string(), channelId: v.id("channels") }).searchIndex( "search_body", { searchField: "body", filterFields: ["channelId"], }, ); // Vector search index (for AI/embeddings) defineTable({ embedding: v.array(v.float64()), text: v.string() }).vectorIndex( "by_embedding", { vectorField: "embedding", dimensions: 1536, }, );
Queries are reactive — clients automatically get updates when data changes.
`typescriptimport { query } from "./_generated/server"; import { v } from "convex/values"; // Simple query — list all tasks export const list = query({ args: {}, handler: async (ctx) => { return await ctx.db.query("tasks").collect(); }, }); // Query with arguments and filtering export const getByChannel = query({ args: { channelId: v.id("channels") }, handler: async (ctx, args) => { return await ctx.db .query("messages") .withIndex("by_channel", (q) => q.eq("channelId", args.channelId)) .order("desc") .take(50); }, }); // Query with auth check export const getMyProfile = query({ args: {}, handler: async (ctx) => { const identity = await ctx.auth.getUserIdentity(); if (!identity) return null; return await ctx.db .query("users") .withIndex("by_token", (q) => q.eq("tokenIdentifier", identity.tokenIdentifier), ) .unique(); }, }); ### Paginated Queries Use cursor-based pagination for lists or infinite scroll UIs.
import { query } from "./_generated/server"; import { paginationOptsValidator } from "convex/server";
export const listPaginated = query({ args: { paginationOpts: paginationOptsValidator }, handler: async (ctx, args) => { return await ctx.db .query("messages") .order("desc") .paginate(args.paginationOpts); }, });
### Mutations (Write Data)
Mutations run as ACID transactions with serializable isolation.
import { mutation } from "./_generated/server"; import { v } from "convex/values";
// Insert a document export const create = mutation({ args: { text: v.string(), isCompleted: v.boolean() }, handler: async (ctx, args) => { const taskId = await ctx.db.insert("tasks", { text: args.text, isCompleted: args.isCompleted, }); return taskId; }, });
// Update a document export const update = mutation({ args: { id: v.id("tasks"), isCompleted: v.boolean() }, handler: async (ctx, args) => { await ctx.db.patch(args.id, { isCompleted: args.isCompleted }); }, });
// Delete a document export const remove = mutation({ args: { id: v.id("tasks") }, handler: async (ctx, args) => { await ctx.db.delete(args.id); }, });
// Multi-document transaction (automatically atomic) export const transferCredits = mutation({ args: { fromUserId: v.id("users"), toUserId: v.id("users"), amount: v.number(), }, handler: async (ctx, args) => { const fromUser = await ctx.db.get(args.fromUserId); const toUser = await ctx.db.get(args.toUserId); if (!fromUser || !toUser) throw new Error("User not found"); if (fromUser.credits < args.amount) throw new Error("Insufficient credits");
await ctx.db.patch(args.fromUserId, { credits: fromUser.credits - args.amount, }); await ctx.db.patch(args.toUserId, { credits: toUser.credits + args.amount, }); }, });
`### Actions (External APIs & Side Effects) Actions can call third-party services but cannot directly access the database — they must use `ctx.runQuery` and `ctx.runMutation`.
import { action } from "./_generated/server"; import { v } from "convex/values"; import { api } from "./_generated/api";
export const sendEmail = action({ args: { to: v.string(), subject: v.string(), body: v.string() }, handler: async (ctx, args) => { // Call external API const response = await fetch("https://api.sendgrid.com/v3/mail/send", { method: "POST", headers: { Authorization: Bearer ${process.env.SENDGRID_API_KEY}, "Content-Type": "application/json", }, body: JSON.stringify({ personalizations: { to: { email: args.to }] }], from: { email: "noreply@example.com" }, subject: args.subject, content: { type: "text/plain", value: args.body }], }), });
if (!response.ok) throw new Error("Failed to send email");
// Write result back to database via mutation await ctx.runMutation(api.emails.recordSent, { to: args.to, subject: args.subject, sentAt: Date.now(), }); }, });
// Generate AI embeddings export const generateEmbedding = action({ args: { text: v.string(), documentId: v.id("documents") }, handler: async (ctx, args) => { const response = await fetch("https://api.openai.com/v1/embeddings", { method: "POST", headers: { Authorization: Bearer ${process.env.OPENAI_API_KEY}, "Content-Type": "application/json", }, body: JSON.stringify({ model: "text-embedding-3-small", input: args.text, }), });
const { data } = await response.json(); await ctx.runMutation(api.documents.saveEmbedding, { documentId: args.documentId, embedding: data0].embedding, }); }, });
### HTTP Actions (Webhooks)
import { httpRouter } from "convex/server"; import { httpAction } from "./_generated/server"; import { api } from "./_generated/api";
const http = httpRouter();
http.route({ path: "/webhooks/stripe", method: "POST", handler: httpAction(async (ctx, request) => { const body = await request.text(); const signature = request.headers.get("stripe-signature");
// Verify webhook signature here...
const event = JSON.parse(body); await ctx.runMutation(api.payments.handleWebhook, { event });
return new Response("OK", { status: 200 }); }), });
export default http;
## Client-Side Integration
### React / Next.js
// app/ConvexClientProvider.tsx "use client"; import { ConvexProvider, ConvexReactClient } from "convex/react"; import { ReactNode } from "react";
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
export function ConvexClientProvider({ children }: { children: ReactNode }) { return <ConvexProvider client={convex}>{children}</ConvexProvider>; }
// app/layout.tsx — wrap children import { ConvexClientProvider } from "./ConvexClientProvider";
export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> <ConvexClientProvider>{children}</ConvexClientProvider> </body> </html> ); }
// Component using Convex hooks "use client"; import { useQuery, useMutation } from "convex/react"; import { api } from "@/convex/_generated/api";
export function TaskList() { // Reactive query — auto-updates when data changes const tasks = useQuery(api.tasks.list); const addTask = useMutation(api.tasks.create); const toggleTask = useMutation(api.tasks.update);
if (tasks === undefined) return <p>Loading...</p>;
return ( <div> {tasks.map((task) => ( <div key={task._id}> <input type="checkbox" checked={task.isCompleted} onChange={() => toggleTask({ id: task._id, isCompleted: !task.isCompleted }) } /> {task.text} </div> ))} <button onClick={() => addTask({ text: "New task", isCompleted: false })}> Add Task </button> </div> ); }
// Component using Paginated Queries "use client"; import { usePaginatedQuery } from "convex/react"; import { api } from "@/convex/_generated/api";
export function MessageLog() { const { results, status, loadMore } = usePaginatedQuery( api.messages.listPaginated, {}, // args { initialNumItems: 20 } );
return ( <div> {results.map((msg) => ( <div key={msg._id}>{msg.body}</div> ))}
{status === "LoadingFirstPage" && <p>Loading...</p>}
{status === "CanLoadMore" && ( <button onClick={() => loadMore(20)}>Load More</button> )} </div> ); }
### With Auth (First-Party Convex Auth)
Convex provides a robust, native authentication library (`@convex-dev/auth`) featuring Magic Links, Passwords, and 80+ OAuth providers without needing a third-party service.
// app/ConvexClientProvider.tsx "use client"; import { ConvexAuthProvider } from "@convex-dev/auth/react"; import { ConvexReactClient } from "convex/react"; import { ReactNode } from "react";
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
export function ConvexClientProvider({ children }: { children: ReactNode }) { return ( <ConvexAuthProvider client={convex}> {children} </ConvexAuthProvider> ); }
// Client-side sign in import { useAuthActions } from "@convex-dev/auth/react";
export function Login() { const { signIn } = useAuthActions(); return <button onClick={() => signIn("github")}>Sign in with GitHub</button>; }
### With Auth (Third-Party Clerk Example)
If you prefer a hosted third-party solution like Clerk:
// app/ConvexClientProvider.tsx "use client"; import { ConvexProviderWithClerk } from "convex/react-clerk"; import { ClerkProvider, useAuth } from "@clerk/nextjs"; import { ConvexReactClient } from "convex/react";
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
export function ConvexClientProvider({ children }: { children: ReactNode }) { return ( <ClerkProvider publishableKey={process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY!}> <ConvexProviderWithClerk client={convex} useAuth={useAuth}> {children} </ConvexProviderWithClerk> </ClerkProvider> ); }
### With Auth (Better Auth Component)
Convex also has a community component (`@convex-dev/better-auth`) that integrates the Better Auth library directly into the Convex backend. This is currently in **early alpha**.
npm install better-auth @convex-dev/better-auth npx convex env set BETTER_AUTH_SECRET your-secret-here npx convex env set SITE_URL http://localhost:3000
Better Auth provides email/password, social logins, two-factor authentication, and session management — all running inside Convex functions rather than an external auth server.
### Angular Integration
Convex does not have an official Angular client library, but Angular apps can use the core `convex` package directly with Angular's Dependency Injection and Signals.
// services/convex.service.ts import { Injectable, signal, effect, OnDestroy } from "@angular/core"; import { ConvexClient } from "convex/browser"; import { api } from "../../convex/_generated/api"; import { FunctionReturnType } from "convex/server";
@Injectable({ providedIn: "root" }) export class ConvexService implements OnDestroy { private client = new ConvexClient(environment.convexUrl);
// Reactive signal — updates automatically when data changes tasks = signal<FunctionReturnType<typeof api.tasks.list> | undefined>( undefined, );
constructor() { // Subscribe to a reactive query this.client.onUpdate(api.tasks.list, {}, (result) => { this.tasks.set(result); }); }
async addTask(text: string) { await this.client.mutation(api.tasks.create, { text, isCompleted: false, }); }
ngOnDestroy() { this.client.close(); } }
// Component usage import { Component, inject } from "@angular/core"; import { ConvexService } from "./services/convex.service";
@Component({ selector: "app-task-list", template: @if (convex.tasks(); as tasks) { @for (task of tasks; track task._id) { <div>{{ task.text }}</div> } } @else { <p>Loading...</p> } <button (click)="convex.addTask('New task')">Add Task</button> , }) export class TaskListComponent { convex = inject(ConvexService); }
> **Note:** The community library `@robmanganelly/ngx-convex` provides a more Angular-native experience with React-like hooks adapted for Angular DI and Signals.
## Scheduling & Cron Jobs
### One-off Scheduled Functions
import { mutation } from "./_generated/server"; import { api } from "./_generated/api";
export const sendReminder = mutation({ args: { userId: v.id("users"), message: v.string(), delayMs: v.number() }, handler: async (ctx, args) => { await ctx.scheduler.runAfter(args.delayMs, api.notifications.send, { userId: args.userId, message: args.message, }); }, });
### Cron Jobs
// convex/crons.ts import { cronJobs } from "convex/server"; import { api } from "./_generated/api";
const crons = cronJobs();
crons.interval("clear old logs", { hours: 24 }, api.logs.clearOld);
crons.cron( "weekly digest", "0 9 1", // Every Monday at 9 AM api.emails.sendWeeklyDigest, );
export default crons;
## File Storage
// Generate an upload URL (mutation) export const generateUploadUrl = mutation({ args: {}, handler: async (ctx) => { return await ctx.storage.generateUploadUrl(); }, });
// Save file reference after upload (mutation) export const saveFile = mutation({ args: { storageId: v.id("_storage"), name: v.string() }, handler: async (ctx, args) => { await ctx.db.insert("files", { storageId: args.storageId, name: args.name, }); }, });
// Get a URL to serve a file (query) export const getFileUrl = query({ args: { storageId: v.id("_storage") }, handler: async (ctx, args) => { return await ctx.storage.getUrl(args.storageId); }, });
## Environment Variables
npx convex env set OPENAI_API_KEY sk-... npx convex env set SENDGRID_API_KEY SG...
npx convex env list
npx convex env unset OPENAI_API_KEY
Access in actions (NOT in queries or mutations):
// Only available in actions const apiKey = process.env.OPENAI_API_KEY;
## Deployment & CLI
npx convex dev
npx convex deploy
npx convex import --table tasks data.jsonl
npx convex export --path ./backup
npx convex dashboard
npx convex run tasks:list
npx convex logs
## Best Practices
- ✅ Define schemas — adds type safety across your entire stack
- ✅ Use indexes for queries — avoids full table scans
- ✅ Use compound indexes with equality filters first, range filter last
- ✅ Rely on native determinism — `Date.now()` and `Math.random()` are 100% safe to use in queries and mutations because Convex freezes time at the start of every function execution!
- ✅ Use `v.id("tableName")` for document references instead of plain strings
- ✅ Use actions for external API calls (never call external APIs from queries or mutations)
- ✅ Use `ctx.runQuery` / `ctx.runMutation` from actions — never access `ctx.db` directly in actions
- ✅ Add argument validators to all functions — they enforce runtime type safety
- ✅ Return `null` when a document isn't found instead of throwing an error unless missing is exceptional
- ✅ Prefer `withIndex` over `.filter()` for query performance
## Anti-Patterns to Avoid
1. **❌ External API calls in queries/mutations**: Only actions can call external services. Queries and mutations run in the Convex transaction engine.
2. **❌ Doing slow CPU-bound work in mutations**: Mutations block database commits; offload heavy processing to actions.
3. **❌ Using `.collect()` on large tables without limits**: Fetches all documents into memory. Use `.take(N)` or `.paginate()`.
4. **❌ Skipping schema definition**: Without a schema you lose end-to-end type safety, the main Convex advantage.
5. **❌ Using `.filter()` instead of indexes**: `.filter()` does a full table scan. Define an index and use `.withIndex()`.
6. **❌ Storing large blobs in documents**: Use Convex file storage (`_storage`) for files; keep documents lean.
7. **❌ Circular `runQuery`/`runMutation` chains**: Actions calling mutations that schedule actions can create infinite loops.
## Common Pitfalls
- **Problem:** "Query returns `undefined` on first render"
**Solution:** This is expected — Convex queries are async. Check for `undefined` before rendering (this means loading, not empty).
- **Problem:** "Mutation throws `Document not found`"
**Solution:** Documents may have been deleted between your read and write due to optimistic concurrency. Re-read inside the mutation.
- **Problem:** "`process.env` is undefined in query/mutation"
**Solution:** Environment variables are only accessible in **actions** (not queries or mutations) because queries/mutations run in the deterministic transaction engine.
- **Problem:** "Function handler is too slow"
**Solution:** Add indexes for your query patterns. Use `withIndex()` instead of `.filter()`. For complex operations, break into smaller mutations.
- **Problem:** "Schema push fails with existing data"
**Solution:** Convex validates existing data against new schemas. Either migrate existing documents first, or use `v.optional()` for new fields.
## Limitations
- Queries and mutations cannot call external HTTP APIs (use actions instead)
- No raw SQL — you work with the Convex query builder API
- Environment variables only available in actions, not in queries or mutations
- Document size limit of 1MB
- Maximum function execution time limits apply
- No server-side rendering of Convex data without specific SSR patterns (use preloading)
- Schemas are enforced at write-time; changing schemas requires data migration for existing documents
## Related Skills
- `@firebase` — Alternative BaaS with Firestore (compare: Convex is TypeScript-first with ACID transactions)
- `@supabase-automation` — Alternative with PostgreSQL backend (compare: Convex is document-relational with built-in reactivity)
- `@prisma-expert` — ORM for traditional databases (Convex replaces both ORM and database)
- `@react-patterns` — Frontend patterns that pair well with Convex React hooks
- `@nextjs-app-router` — Next.js App Router integration patterns
- `@authentication-oauth` — Auth patterns (Convex supports Clerk, Auth0, Convex Auth)
- `@stripe` — Payment integration via Convex actions and HTTP webhooks
## Resources
- [Official Docs](https://docs.convex.dev)
- [Convex Stack (Blog)](https://stack.convex.dev)
- [GitHub](https://github.com/get-convex/convex-backend)
- [Discord Community](https://convex.dev/community)
- [Convex Chef (AI Starter)](https://chef.convex.dev)Other measured skills in the registry, with their headline benchmark lift.