convex-core

Core Convex development guidelines - functions, validators, schema, queries, mutations, and database patterns

$ 설치

git clone https://github.com/majiayu000/claude-skill-registry /tmp/claude-skill-registry && cp -r /tmp/claude-skill-registry/skills/data/convex-core ~/.claude/skills/claude-skill-registry

// tip: Run this command in your terminal to install the skill


name: convex-core description: Core Convex development guidelines - functions, validators, schema, queries, mutations, and database patterns globs:

  • "convex/**/*.ts"
  • "convex/schema.ts" triggers:
  • convex
  • query
  • mutation
  • schema
  • validator
  • index
  • database
  • ctx.db
  • defineTable
  • defineSchema

Convex Core Development Guide

Complete guidelines for Convex functions, validators, schema, queries, mutations, and database patterns.


Function Syntax (REQUIRED)

ALWAYS use the new function syntax:

import { query } from "./_generated/server";
import { v } from "convex/values";

export const myFunction = query({
  args: {
    // Arguments with validators
  },
  returns: v.null(), // Return validator REQUIRED
  handler: async (ctx, args) => {
    // Function body
  },
});

Validators Reference

TypeValidatorNotes
Idv.id(tableName)Document ID
Nullv.null()Use instead of undefined
Int64v.int64()NOT v.bigint() (deprecated)
Float64v.number()
Booleanv.boolean()
Stringv.string()Max 1MB UTF-8
Bytesv.bytes()Max 1MB
Arrayv.array(values)Max 8192 elements
Objectv.object({...})Max 1024 entries
Recordv.record(keys, values)Dynamic keys
Optionalv.optional(validator)
Unionv.union(v1, v2, ...)
Literalv.literal("value")For discriminated unions

NOT SUPPORTED: v.map(), v.set(), v.bigint()


Function Registration

Public Functions

import { query, mutation, action } from "./_generated/server";
import { api } from "./_generated/api";
// Reference: api.filename.functionName

Internal Functions (Private)

import { internalQuery, internalMutation, internalAction } from "./_generated/server";
import { internal } from "./_generated/api";
// Reference: internal.filename.functionName

Critical Rules

  • ALWAYS include args and returns validators
  • If no return value, use returns: v.null()
  • File-based routing: convex/users.tsapi.users.functionName

Schema Definition

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

export default defineSchema({
  users: defineTable({
    name: v.string(),
    email: v.string(),
    role: v.optional(v.union(v.literal("admin"), v.literal("user"))),
  })
    .index("by_email", ["email"])
    .index("by_role", ["role"]),

  messages: defineTable({
    userId: v.id("users"),
    content: v.string(),
    channelId: v.id("channels"),
  })
    .index("by_channel", ["channelId"])
    .index("by_user_and_channel", ["userId", "channelId"]),
});

System Fields (Automatic)

  • _id: v.id(tableName)
  • _creationTime: v.number()

Index Rules (CRITICAL)

// WRONG - Will cause error!
.index("by_creation_time", ["_creationTime"])  // Built-in, don't add
.index("by_author_and_time", ["author", "_creationTime"])  // _creationTime is automatic

// CORRECT
.index("by_author", ["author"])  // _creationTime added automatically
.index("by_channel_and_author", ["channelId", "authorId"])
  • Index names describe all fields: by_field1_and_field2
  • Query order must match index order
  • Never include _creationTime in index definition

Query Patterns

Using Indexes (REQUIRED)

// CORRECT - Use withIndex
const messages = await ctx.db
  .query("messages")
  .withIndex("by_channel", (q) => q.eq("channelId", channelId))
  .order("desc")
  .take(10);

// WRONG - Never use filter()
const messages = await ctx.db
  .query("messages")
  .filter((q) => q.eq(q.field("channelId"), channelId))  // BAD!
  .collect();

Get by ID

const user = await ctx.db.get(userId);
if (!user) throw new Error("User not found");

Ordering

.order("asc")   // Ascending (default)
.order("desc")  // Descending

Collecting Results

.collect()     // Get all results
.take(n)       // Get first n results
.first()       // Get first result or null
.unique()      // Get single result, throws if multiple

Mutations

// Insert - returns Id
const id = await ctx.db.insert("users", { name: "Alice", email: "alice@example.com" });

// Patch - partial update
await ctx.db.patch(userId, { name: "Bob" });

// Replace - full replacement
await ctx.db.replace(userId, { name: "Bob", email: "bob@example.com" });

// Delete
await ctx.db.delete(userId);

Delete Pattern (No .delete() on queries)

const items = await ctx.db
  .query("items")
  .withIndex("by_user", (q) => q.eq("userId", userId))
  .collect();

for (const item of items) {
  await ctx.db.delete(item._id);
}

Function Calling

// From mutation or action
const user = await ctx.runQuery(api.users.get, { id: userId });
await ctx.runMutation(internal.users.update, { id: userId, name });

// From action only
await ctx.runAction(internal.ai.generate, { prompt });

Type Annotation for Same-File Calls

export const f = query({
  args: { name: v.string() },
  returns: v.string(),
  handler: async (ctx, args) => "Hello " + args.name,
});

export const g = query({
  args: {},
  returns: v.null(),
  handler: async (ctx) => {
    const result: string = await ctx.runQuery(api.example.f, { name: "Bob" });
    return null;
  },
});

Pagination

import { paginationOptsValidator } from "convex/server";

export const list = query({
  args: {
    paginationOpts: paginationOptsValidator,
    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")
      .paginate(args.paginationOpts);
  },
});
// Returns: { page, isDone, continueCursor }

Full Text Search

// Schema
defineTable({
  body: v.string(),
  channel: v.string(),
}).searchIndex("search_body", {
  searchField: "body",
  filterFields: ["channel"],
})

// Query
const results = await ctx.db
  .query("messages")
  .withSearchIndex("search_body", (q) =>
    q.search("body", "hello").eq("channel", "#general")
  )
  .take(10);

System Limits

LimitValue
Function args/returns8 MiB
Array elements8192
Object entries1024
Document size1 MiB
Query/Mutation timeout1 second
DB read per query8 MiB / 16384 docs
DB write per mutation8 MiB / 8192 docs

TypeScript Types

import { Id, Doc } from "./_generated/dataModel";

// Use Id<> for document IDs
function getUser(userId: Id<"users">): Promise<Doc<"users"> | null>

// Record with Id keys
const map: Record<Id<"users">, string> = {};

// Arrays with explicit types
const items: Array<{ id: Id<"items">; name: string }> = [];