Skip to main content
Technology & EngineeringBaas292 lines

Convex

Convex real-time backend with reactive queries, mutations, and serverless functions

Quick Summary26 lines
You are an expert in Convex for rapid backend development, including its reactive database, server functions, file storage, scheduling, and real-time sync.

## Key Points

- Define a schema in `convex/schema.ts` — it provides type safety across your entire stack and enables Convex to validate data at write time.
- Use indexes for any query that filters or sorts; Convex does not scan tables without indexes.
- Keep queries and mutations deterministic (no `Date.now()`, no `Math.random()`) — use `action` for side effects.
- Use `ctx.scheduler.runAfter` to defer slow work (email, webhooks) from mutations to actions.
- Leverage the automatic real-time sync: `useQuery` re-renders when data changes with zero extra code.
- **Side effects in queries/mutations**: Queries and mutations run in a deterministic sandbox — external API calls, `Date.now()`, and random number generation must go in `action` functions.
- **Not using indexes**: Queries without an appropriate index will throw an error at runtime.
- **Large transactions**: Mutations that read or write too many documents in a single transaction can hit Convex limits; break large operations into batches using `ctx.scheduler`.
- **Forgetting argument validation**: Always define `args` with `v` validators; this ensures type safety and prevents malicious inputs.
- **Ignoring pagination**: For large result sets, use `.paginate(opts)` instead of `.collect()` to avoid loading everything into memory.

## Quick Example

```bash
# Start the Convex dev server (watches for changes, pushes functions)
npx convex dev

# Deploy to production
npx convex deploy
```
skilldb get baas-skills/ConvexFull skill: 292 lines
Paste into your CLAUDE.md or agent config

Convex — Backend as a Service

You are an expert in Convex for rapid backend development, including its reactive database, server functions, file storage, scheduling, and real-time sync.

Core Philosophy

Overview

Convex is a full-stack reactive backend platform. It provides a document database with automatic real-time sync to clients, server-side functions (queries, mutations, actions) written in TypeScript/JavaScript, file storage, cron jobs, and built-in search. All server functions run in a transactional, deterministic runtime. Convex eliminates the need for manual caching, WebSocket plumbing, or separate API layers.

Setup & Configuration

Project Initialization

# Create a new project
npm create convex@latest

# Or add to an existing project
npm install convex
npx convex init

Convex Provider (React)

// src/main.tsx
import { ConvexProvider, ConvexReactClient } from 'convex/react';

const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL);

function App() {
  return (
    <ConvexProvider client={convex}>
      <MainApp />
    </ConvexProvider>
  );
}

Development Workflow

# Start the Convex dev server (watches for changes, pushes functions)
npx convex dev

# Deploy to production
npx convex deploy

Core Patterns

Schema Definition

// convex/schema.ts
import { defineSchema, defineTable } from 'convex/server';
import { v } from 'convex/values';

export default defineSchema({
  posts: defineTable({
    title: v.string(),
    body: v.string(),
    authorId: v.id('users'),
    published: v.boolean(),
  })
    .index('by_author', ['authorId'])
    .index('by_published', ['published']),

  users: defineTable({
    name: v.string(),
    email: v.string(),
    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: { published: v.optional(v.boolean()) },
  handler: async (ctx, args) => {
    if (args.published !== undefined) {
      return await ctx.db
        .query('posts')
        .withIndex('by_published', (q) => q.eq('published', args.published!))
        .order('desc')
        .take(20);
    }
    return await ctx.db.query('posts').order('desc').take(20);
  },
});

export const get = query({
  args: { id: v.id('posts') },
  handler: async (ctx, args) => {
    return await 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(),
    body: 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 await ctx.db.insert('posts', {
      title: args.title,
      body: args.body,
      authorId: user._id,
      published: false,
    });
  },
});

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 response = 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@example.com',
        to: args.to,
        subject: args.subject,
        html: args.body,
      }),
    });

    // Actions can call mutations to write back to the database
    await ctx.runMutation(api.logs.create, {
      type: 'email_sent',
      details: `Email sent to ${args.to}`,
    });

    return response.ok;
  },
});

Using from React

import { useQuery, useMutation } from 'convex/react';
import { api } from '../convex/_generated/api';

function PostList() {
  // Automatically re-renders when data changes
  const posts = useQuery(api.posts.list, { published: true });
  const createPost = useMutation(api.posts.create);

  if (posts === undefined) return <div>Loading...</div>;

  return (
    <div>
      <button onClick={() => createPost({ title: 'New', body: 'Content' })}>
        Add Post
      </button>
      {posts.map((post) => (
        <div key={post._id}>{post.title}</div>
      ))}
    </div>
  );
}

File Storage

// Generate upload URL (mutation)
export const generateUploadUrl = mutation(async (ctx) => {
  return await ctx.storage.generateUploadUrl();
});

// Store the storage ID after upload (mutation)
export const saveFile = mutation({
  args: { storageId: v.id('_storage'), postId: v.id('posts') },
  handler: async (ctx, args) => {
    await ctx.db.patch(args.postId, { imageId: args.storageId });
  },
});

// Get the URL for a stored file (query)
export const getImageUrl = query({
  args: { storageId: v.id('_storage') },
  handler: async (ctx, args) => {
    return await ctx.storage.getUrl(args.storageId);
  },
});

Authentication (with Clerk)

// convex/auth.config.ts
export default {
  providers: [
    {
      domain: process.env.CLERK_JWT_ISSUER_DOMAIN,
      applicationID: 'convex',
    },
  ],
};
// Client-side integration
import { ConvexProviderWithClerk } from 'convex/react-clerk';
import { ClerkProvider, useAuth } from '@clerk/clerk-react';

<ClerkProvider publishableKey={CLERK_KEY}>
  <ConvexProviderWithClerk client={convex} useAuth={useAuth}>
    <App />
  </ConvexProviderWithClerk>
</ClerkProvider>

Best Practices

  • Define a schema in convex/schema.ts — it provides type safety across your entire stack and enables Convex to validate data at write time.
  • Use indexes for any query that filters or sorts; Convex does not scan tables without indexes.
  • Keep queries and mutations deterministic (no Date.now(), no Math.random()) — use action for side effects.
  • Use ctx.scheduler.runAfter to defer slow work (email, webhooks) from mutations to actions.
  • Leverage the automatic real-time sync: useQuery re-renders when data changes with zero extra code.

Common Pitfalls

  • Side effects in queries/mutations: Queries and mutations run in a deterministic sandbox — external API calls, Date.now(), and random number generation must go in action functions.
  • Not using indexes: Queries without an appropriate index will throw an error at runtime.
  • Large transactions: Mutations that read or write too many documents in a single transaction can hit Convex limits; break large operations into batches using ctx.scheduler.
  • Forgetting argument validation: Always define args with v validators; this ensures type safety and prevents malicious inputs.
  • Ignoring pagination: For large result sets, use .paginate(opts) instead of .collect() to avoid loading everything into memory.

Anti-Patterns

Over-engineering for hypothetical requirements. Building for scenarios that may never materialize adds complexity without value. Solve the problem in front of you first.

Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide wastes time and introduces risk.

Premature abstraction. Creating elaborate frameworks before having enough concrete cases to know what the abstraction should look like produces the wrong abstraction.

Neglecting error handling at system boundaries. Internal code can trust its inputs, but boundaries with external systems require defensive validation.

Skipping documentation. What is obvious to you today will not be obvious to your colleague next month or to you next year.

Install this skill directly: skilldb add baas-skills

Get CLI access →