StarterApp Docs
Database

AI Patterns

How AI agents implement database features in this codebase

This guide documents patterns enabling AI agents to implement Convex database features correctly on first attempt. Focus: How AI uses these patterns, not general Convex documentation.

For AI Agents

This page explains the conventions you must follow when implementing database features. The patterns in convex/tables.ts and convex/_helpers/builders.ts are mandatory—not suggestions.

AI Capabilities

What AI agents can implement using these patterns:

Full CRUD Operations

Create, read, update, delete with automatic auth, rate limiting, and error handling

Real-Time Features

Reactive queries, subscriptions, presence, optimistic updates via builder wrappers

Billing Integration

Feature gates and usage tracking via ctx.check() and ctx.track() in userAction

Security by Default

Auto-scoped queries, ownership verification, typed errors, concealExistence pattern

Context Engineering Architecture

AI agents have access to these discoverable patterns:

Tables Pattern

convex/tables.ts - Never hardcode table names or validators

Builder Functions

convex/_helpers/builders.ts - Auto-inject auth and guards

Type Safety

Generated types from schema ensure compile-time correctness

Error Handling

convex/_helpers/errors.ts - Typed errors with HTTP semantics

The Tables Pattern (Critical)

AI Rule #1: Never hardcode table names. Always import from convex/tables.ts:

import { T, V, type UserId, type SupportTicketId } from "./tables";
  • T - Table name constants (T.supportTickets)
  • V - Validator helpers (V.supportTicketId)
  • Types - Type aliases (UserId, SupportTicketId)

Usage:

// ✅ Correct
args: { id: V.supportTicketId }
await ctx.db.insert(T.supportTickets, { ... })

// ❌ Wrong
args: { id: v.id("supportTickets") }
await ctx.db.insert("supportTickets", { ... })

When Adding New Tables

Update convex/tables.ts with: (1) table constant in T, (2) validator in V, (3) type alias. Then run pnpm codegen.

Builder Pattern (Auth Guards)

AI Rule #2: Always use builders. Never use raw query, mutation, or action:

import { userQuery, userMutation, userAction } from "./_helpers/builders";

What builders provide:

  • ✅ Automatic authentication (ctx.viewerId injected)
  • ✅ Auto-scoped ctx.db for user-owned tables
  • ✅ Billing gates (ctx.check()) and tracking (ctx.track())
  • ✅ Typed errors (throws UNAUTHENTICATED, FORBIDDEN, etc.)

When to use each:

  • userQuery - Read operations (no external calls)
  • userMutation - Write operations (insert, patch, delete)
  • userAction - External APIs, billing gates, heavy compute

Example:

export const createTicket = userMutation({
  args: { title: v.string() },
  handler: async (ctx, { title }) => {
    // ctx.viewerId is authenticated and available
    return await ctx.db.insert(T.supportTickets, {
      userId: ctx.viewerId as UserId,
      title,
      status: "Open",
      updatedAt: Date.now(),
    });
  },
});

Schema Conventions

AI Rule #3: Follow schema patterns. Standard conventions:

myTable: defineTable({
  userId: v.id("user"),           // Always link to user
  status: v.union(                // Enums via union of literals
    v.literal("Active"),
    v.literal("Inactive")
  ),
  updatedAt: v.number(),          // Unix timestamp for updates
  // Use _creationTime instead of createdAt
})
  .index("by_user_id", ["userId"]) // Index for common queries
  .searchIndex("search_field", {   // Full-text search if needed
    searchField: "title",
    filterFields: ["userId"],
  })

Key patterns:

  • Singular table names (user not users)
  • Required fields (avoid v.optional() unless necessary)
  • updatedAt for mutations, _creationTime for created timestamp
  • Indexes for foreign keys and common filters
  • Search indexes for text queries

Error Handling & Authorization

AI Rule #4: Use typed errors. Import from convex/_helpers/errors.ts:

import { AppError } from "./_helpers/errors";
import { assertOwnerByUserId } from "./lib/identity";

export const updateTicket = userMutation({
  args: { ticketId: V.supportTicketId, title: v.string() },
  handler: async (ctx, { ticketId, title }) => {
    const ticket = await ctx.db.get(ticketId);

    // Security: Conceal resource existence
    assertOwnerByUserId(ticket, ctx.viewerId as UserId, {
      concealExistence: true  // Throws NOT_FOUND instead of FORBIDDEN
    });

    await ctx.db.patch(ticketId, { title, updatedAt: Date.now() });
  },
});

Available error codes:

  • UNAUTHENTICATED (401) - Not logged in (auto-thrown by builders)
  • FORBIDDEN (403) - Not authorized
  • NOT_FOUND (404) - Resource not found
  • RATE_LIMITED (429) - Too many requests
  • PLAN_REQUIRED (402) - Billing gate failed

Security Pattern

Always use concealExistence: true to prevent information leakage about resource existence.

Rate Limiting

AI Rule #5: Add rate limits to mutations. Import from convex/rateLimits.ts:

import { assertLimitAction } from "./rateLimits";

export const createTicket = userMutation({
  args: { title: v.string() },
  handler: async (ctx, { title }) => {
    // 10 requests per minute
    await assertLimitAction(ctx, {
      scope: "support:create",
      viewerId: ctx.viewerId as UserId,
      max: 10,
      windowMs: 60_000,
    });

    // Rest of mutation...
  },
});

When to apply:

  • Create operations (prevent spam)
  • Update operations (prevent abuse)
  • External API calls (protect quota)
  • Expensive operations (protect resources)

Naming Conventions

Critical: Singular Table Names

Use SINGULAR table names: user not users, supportTickets not support_tickets.

AI must follow:

  • ✅ Singular: user, session, supportTickets
  • ✅ camelCase for compound words: billingCustomers
  • ❌ Never plural: users, sessions
  • ❌ Never snake_case: billing_customers

AI Implementation Checklist

When implementing new database features, follow this exact order:

newTable: defineTable({
  userId: v.id("user"),
  title: v.string(),
  updatedAt: v.number(),
}).index("by_user_id", ["userId"])
// Add to T object
newTable: "newTable",

// Add type alias
export type NewTableId = Id<typeof T.newTable>;

// Add to V object
newTableId: v.id(T.newTable),
import { userQuery, userMutation } from "./_helpers/builders";
import { T, V, type UserId, type NewTableId } from "./tables";

export const create = userMutation({
  args: { title: v.string() },
  handler: async (ctx, { title }) => {
    return await ctx.db.insert(T.newTable, {
      userId: ctx.viewerId as UserId,
      title,
      updatedAt: Date.now(),
    });
  },
});

export const list = userQuery({
  args: {},
  handler: async (ctx) => {
    return await ctx.db.query(T.newTable).collect();
  },
});
pnpm codegen

Common Patterns AI Can Implement

Testing Pattern

AI can write tests using convex-test:

import { convexTest } from "convex-test";
import { expect, test } from "vitest";
import schema from "../schema";
import { api } from "../_generated/api";

test("create and query", async () => {
  const t = convexTest(schema);
  t.setAuth({ subject: "user123" });  // Mock auth

  const id = await t.mutation(api.feature.create, {
    title: "Test",
  });

  const results = await t.query(api.feature.list, {});
  expect(results).toHaveLength(1);
  expect(results[0]._id).toBe(id);
});

AI Context Files

AI agents read these files to understand patterns:

FilePurpose
llms/CONVEX.mdCore patterns, conventions, implementation rules
convex/schema.tsCurrent tables, indexes, validation patterns
convex/tables.tsTable constants (T), validators (V), type aliases
convex/_helpers/builders.tsBuilder implementations, auto-scoping logic
convex/support.tsWorking example of all patterns in production

For AI: Before Implementing

Always read llms/CONVEX.md and examine convex/tables.ts before implementing new database features.

Anti-Patterns (What NOT to Do)

These Will Break the Codebase

AI must avoid these common mistakes:

// ❌ Hardcoded table names
args: { id: v.id("supportTickets") }
await ctx.db.query("supportTickets")

// ✅ Use constants from tables.ts
args: { id: V.supportTicketId }
await ctx.db.query(T.supportTickets)

// ❌ Bypassing builders
export const myQuery = query({ ... })

// ✅ Always use builders
export const myQuery = userQuery({ ... })

// ❌ Plural table names
users: defineTable({ ... })

// ✅ Singular names
user: defineTable({ ... })

// ❌ Forgetting to update tables.ts
// When adding new tables to schema.ts

// ✅ Update T, V, and types
T.newTable = "newTable"
type NewTableId = Id<typeof T.newTable>
V.newTableId = v.id(T.newTable)

// ❌ Skipping codegen
// After schema changes

// ✅ Always run
pnpm codegen