Convex
Build with Convex as a reactive backend. Use this skill when the project needs to integrate Convex for real-time database, server functions, file storage, scheduled jobs, authentication, or full-stack TypeScript development with automatic reactivity. Covers the Convex client, queries, mutations, actions, schema validation, indexes, real-time subscriptions, and deployment.
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: 306 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
Drizzle
Use Drizzle ORM for type-safe SQL in TypeScript. Use this skill when the project needs to integrate Drizzle for SQL-first database access, schema definition in TypeScript, migrations, relational queries, or connecting to PostgreSQL, MySQL, SQLite, Turso, Neon, PlanetScale, or Vercel Postgres. Covers Drizzle schema, query builder, relational API, migrations, and driver integrations.
Dynamodb
Build with Amazon DynamoDB as a serverless NoSQL database. Use this skill when the project needs to integrate DynamoDB for key-value and document storage, single-table design, global tables, streams, TTL, or serverless-first data access. Covers the AWS SDK v3, single-table design, GSIs, queries, scans, transactions, and DynamoDB Streams.
Fauna
Build with Fauna as a distributed document-relational database. Use this skill when the project needs to integrate Fauna for globally distributed data, ACID transactions, document-relational modeling, FQL queries, or serverless database access. Covers the Fauna v10 client, FQL, schema, indexes, and authentication.
Firebase Firestore
Build with Firebase Firestore as a NoSQL document database. Use this skill when the project needs to integrate Firestore for real-time document storage, offline persistence, security rules, queries, aggregations, or Cloud Functions triggers. Covers the Firestore SDK (v9+), data modeling, queries, security rules, real-time listeners, batched writes, transactions, and Cloud Functions integration.
Mongodb
Build with MongoDB as a document database. Use this skill when the project needs to integrate MongoDB or Mongoose for document storage, flexible schemas, aggregation pipelines, Atlas Search, change streams, or transactions. Covers the MongoDB Node.js driver, Mongoose ODM, schema design, indexing, aggregation, and Atlas features.
Neo4j
Build with Neo4j for graph data and relationship-heavy queries. Use this skill when the project needs a graph database for social networks, recommendation engines, knowledge graphs, fraud detection, or any domain where relationships between entities are central. Covers Cypher query language, the Neo4j JavaScript driver, graph modeling, and common graph patterns.