Convex
Build with Convex as a reactive backend. Use this skill when the project needs
You are a backend specialist who integrates Convex into projects. Convex is a reactive backend platform where database queries automatically update the UI when data changes — no manual subscriptions, polling, or cache invalidation needed. ## Key Points - Define your schema first — it drives type generation and validation - Use indexes for every query pattern — Convex requires indexed queries - Use queries for reads, mutations for writes, actions for external API calls - Actions cannot read/write the database directly — call mutations from actions - Use `ctx.auth.getUserIdentity()` in mutations for authorization - Keep mutations fast — they run in transactions and block other writes - Use `v.optional()` for fields that may not exist on older documents - Calling external APIs from queries or mutations — use actions instead - Not defining indexes for query patterns — queries will fail - Using `Date.now()` in queries — queries must be deterministic - Storing large blobs in the database — use file storage - Not handling the `undefined` state from `useQuery` (data still loading) ## Quick Example ```bash npm install convex npx convex dev # Starts dev server and syncs functions ```
skilldb get database-services-skills/ConvexFull skill: 307 linesConvex Integration
You are a backend specialist who integrates Convex into projects. Convex is a reactive backend platform where database queries automatically update the UI when data changes — no manual subscriptions, polling, or cache invalidation needed.
Core Philosophy
Reactivity is built in
When a Convex query's data changes, every client subscribed to that query gets updated automatically. You don't write subscription code, invalidate caches, or poll for changes. The reactivity is in the platform, not your code.
Functions are the API
Your backend is TypeScript functions — queries (read), mutations (write), and actions (side effects). No REST endpoints to design, no GraphQL schemas to maintain. Functions are your API surface.
Schema is enforced at runtime
Convex validates data against your schema on every write. TypeScript types are generated from the schema, giving you end-to-end type safety from database to UI.
Setup
Install
npm install convex
npx convex dev # Starts dev server and syncs functions
Initialize (React)
// convex/_generated/api.ts is auto-generated
import { ConvexProvider, ConvexReactClient } from 'convex/react';
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
function App() {
return (
<ConvexProvider client={convex}>
<MyApp />
</ConvexProvider>
);
}
Key Techniques
Schema definition
// convex/schema.ts
import { defineSchema, defineTable } from 'convex/server';
import { v } from 'convex/values';
export default defineSchema({
posts: defineTable({
title: v.string(),
content: v.string(),
authorId: v.id('users'),
status: v.union(v.literal('draft'), v.literal('published')),
tags: v.array(v.string()),
createdAt: v.number(),
})
.index('by_author', ['authorId'])
.index('by_status', ['status', 'createdAt'])
.searchIndex('search_posts', {
searchField: 'title',
filterFields: ['status'],
}),
users: defineTable({
name: v.string(),
email: v.string(),
plan: v.union(v.literal('free'), v.literal('pro'), v.literal('enterprise')),
tokenIdentifier: v.string(),
}).index('by_token', ['tokenIdentifier']),
});
Queries (reactive reads)
// convex/posts.ts
import { query } from './_generated/server';
import { v } from 'convex/values';
export const list = query({
args: { status: v.optional(v.string()) },
handler: async (ctx, args) => {
if (args.status) {
return ctx.db
.query('posts')
.withIndex('by_status', (q) => q.eq('status', args.status))
.order('desc')
.take(20);
}
return ctx.db.query('posts').order('desc').take(20);
},
});
export const get = query({
args: { id: v.id('posts') },
handler: async (ctx, args) => {
return ctx.db.get(args.id);
},
});
Mutations (writes)
// convex/posts.ts
import { mutation } from './_generated/server';
import { v } from 'convex/values';
export const create = mutation({
args: {
title: v.string(),
content: v.string(),
},
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error('Not authenticated');
const user = await ctx.db
.query('users')
.withIndex('by_token', (q) => q.eq('tokenIdentifier', identity.tokenIdentifier))
.unique();
if (!user) throw new Error('User not found');
return ctx.db.insert('posts', {
title: args.title,
content: args.content,
authorId: user._id,
status: 'draft',
tags: [],
createdAt: Date.now(),
});
},
});
export const update = mutation({
args: {
id: v.id('posts'),
title: v.optional(v.string()),
content: v.optional(v.string()),
status: v.optional(v.union(v.literal('draft'), v.literal('published'))),
},
handler: async (ctx, args) => {
const { id, ...fields } = args;
await ctx.db.patch(id, fields);
},
});
export const remove = mutation({
args: { id: v.id('posts') },
handler: async (ctx, args) => {
await ctx.db.delete(args.id);
},
});
Actions (side effects)
// convex/actions.ts
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) => {
// Actions can call external APIs
const res = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.RESEND_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: 'noreply@yourdomain.com',
to: args.to,
subject: args.subject,
html: args.body,
}),
});
// Actions can call mutations to write data
await ctx.runMutation(api.emails.logSend, {
to: args.to,
subject: args.subject,
sentAt: Date.now(),
});
},
});
Using in React (automatic reactivity)
import { useQuery, useMutation } from 'convex/react';
import { api } from '../convex/_generated/api';
function PostList() {
// This automatically updates when posts change — no manual refresh
const posts = useQuery(api.posts.list, { status: 'published' });
const createPost = useMutation(api.posts.create);
if (posts === undefined) return <div>Loading...</div>;
return (
<div>
{posts.map(post => <PostCard key={post._id} post={post} />)}
<button onClick={() => createPost({ title: 'New', content: '...' })}>
Create Post
</button>
</div>
);
}
Scheduled functions (cron)
// convex/crons.ts
import { cronJobs } from 'convex/server';
import { api } from './_generated/api';
const crons = cronJobs();
crons.interval('cleanup drafts', { hours: 24 }, api.posts.cleanupOldDrafts);
crons.cron('weekly digest', '0 9 * * 1', api.emails.sendWeeklyDigest);
export default crons;
File storage
// Generate upload URL (mutation)
export const generateUploadUrl = mutation(async (ctx) => {
return ctx.storage.generateUploadUrl();
});
// Store file reference (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,
uploadedAt: Date.now(),
});
},
});
// Get file URL (query)
export const getFileUrl = query({
args: { storageId: v.id('_storage') },
handler: async (ctx, args) => {
return ctx.storage.getUrl(args.storageId);
},
});
Full-text search
export const searchPosts = query({
args: { query: v.string() },
handler: async (ctx, args) => {
return ctx.db
.query('posts')
.withSearchIndex('search_posts', (q) =>
q.search('title', args.query).eq('status', 'published')
)
.take(10);
},
});
Best Practices
- Define your schema first — it drives type generation and validation
- Use indexes for every query pattern — Convex requires indexed queries
- Use queries for reads, mutations for writes, actions for external API calls
- Actions cannot read/write the database directly — call mutations from actions
- Use
ctx.auth.getUserIdentity()in mutations for authorization - Keep mutations fast — they run in transactions and block other writes
- Use
v.optional()for fields that may not exist on older documents
Anti-Patterns
- Calling external APIs from queries or mutations — use actions instead
- Not defining indexes for query patterns — queries will fail
- Using
Date.now()in queries — queries must be deterministic - Storing large blobs in the database — use file storage
- Not handling the
undefinedstate fromuseQuery(data still loading) - Making mutations do heavy computation — they hold a write lock
- Skipping schema definition — lose type safety and runtime validation
Install this skill directly: skilldb add database-services-skills
Related Skills
Cassandra
Build with Apache Cassandra for high-availability distributed data. Use this skill
Clickhouse
Build with ClickHouse for real-time analytics and OLAP workloads. Use this skill
Cockroachdb
Build with CockroachDB as a distributed SQL database. Use this skill when the
Drizzle
Use Drizzle ORM for type-safe SQL in TypeScript. Use this skill when the project
Dynamodb
Build with Amazon DynamoDB as a serverless NoSQL database. Use this skill when
Fauna
Build with Fauna as a distributed document-relational database. Use this skill