Files
the-pet-loft/convex/CLAUDE.md
ianshaloom 0f91d3dc05 feat(convex): add implementation rules and configuration for backend functions
- Introduced a comprehensive markdown document outlining implementation rules for Convex functions, including syntax, registration, HTTP endpoints, and TypeScript usage.
- Created a new configuration file for the Convex app, integrating the Resend service.
- Added a new HTTP route for handling Shippo webhooks to ensure proper response handling.
- Implemented integration tests for order timeline events, covering various scenarios including order fulfillment and status changes.
- Enhanced existing functions with type safety improvements and additional validation logic.

This commit establishes clear guidelines for backend development and improves the overall structure and reliability of the Convex application.
2026-03-07 19:32:06 +03:00

7.9 KiB

Convex Backend — Implementation Rules

Applies to all files inside convex/. These rules govern how to write, organize, and call Convex functions in this project.


1. Function Syntax

Always use the new object syntax with explicit args and handler:

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

export const myFunction = query({
  args: { id: v.id("products") },
  handler: async (ctx, args) => {
    // ...
  },
});

2. Function Registration

Visibility Decorators to use
Public API query, mutation, action
Private / internal internalQuery, internalMutation, internalAction
  • Always include argument validators on every function — public and internal alike.
  • Do NOT register functions through the api or internal objects — those are for calling, not registering.
  • If a function returns nothing, it implicitly returns null. Do not return undefined.

3. HTTP Endpoints

Define all HTTP endpoints in convex/http.ts using httpAction:

import { httpRouter } from "convex/server";
import { httpAction } from "./_generated/server";

const http = httpRouter();

http.route({
  path: "/api/some-route",
  method: "POST",
  handler: httpAction(async (ctx, req) => {
    const body = await req.bytes();
    return new Response(body, { status: 200 });
  }),
});

Endpoints are registered at the exact path specified — no automatic prefix is added.


4. Function Calling

  • ctx.runQuery — call a query from a query, mutation, or action
  • ctx.runMutation — call a mutation from a mutation or action
  • ctx.runAction — call an action from an action (only cross-runtime: V8 → Node)
  • All calls take a FunctionReference — never pass the function directly
  • Minimize query/mutation calls from actions; splitting logic creates race conditions
  • When calling a function in the same file, add a type annotation on the return value to avoid TypeScript circularity errors:
const result: string = await ctx.runQuery(api.example.f, { name: "Bob" });

5. Function References

  • Public functions → api object from convex/_generated/api.ts
  • Internal functions → internal object from convex/_generated/api.ts
  • File-based routing: convex/products.tsapi.products.myFn; convex/model/users.tsapi.model.users.myFn

6. Validators

Use v from convex/values. Valid types:

Convex Type Validator Notes
Id v.id("tableName")
String v.string() UTF-8, max 1 MB
Number v.number() IEEE-754 float64
Boolean v.boolean()
Null v.null() Use instead of undefined
Int64 v.int64() BigInt between -2^63 and 2^63-1
Bytes v.bytes() ArrayBuffer, max 1 MB
Array v.array(values) Max 8192 elements
Object v.object({...}) Max 1024 entries; keys cannot start with $ or _
Record v.record(keys, values) Dynamic keys; ASCII only, no $/_ prefix
Union v.union(...) Discriminated unions supported
Optional v.optional(...)
  • v.bigint() is deprecated — use v.int64() instead
  • v.map() and v.set() are not supported — use v.record() instead

7. Schema

  • Define all tables in convex/schema.ts
  • Import from convex/server: defineSchema, defineTable
  • System fields (_id, _creationTime) are added automatically — do not define them
  • Index naming: always include all indexed fields — e.g. ["field1", "field2"] → name "by_field1_and_field2"
  • Index fields must be queried in the same order they are defined; create separate indexes for different orderings

8. TypeScript

  • Use Id<"tableName"> from ./_generated/dataModel for document ID types — not plain string
  • Use Doc<"tableName"> for full document types
  • Use as const for string literals in discriminated unions
  • Declare arrays as const arr: Array<T> = [...]
  • Declare records as const rec: Record<K, V> = {...}

9. Queries

  • Never use .filter() — define an index in the schema and use .withIndex() instead
  • Use .unique() to assert a single result (throws if multiple match)
  • Use .take(n) or .paginate() to limit results
  • Prefer for await (const row of query) for async iteration over .collect() + loop
  • Convex does not support .delete() on queries — collect results then ctx.db.delete(row._id) each one
  • Default sort order is ascending _creationTime; use .order("asc" | "desc") to override
const results = await ctx.db
  .query("products")
  .withSearchIndex("search_name", (q) =>
    q.search("name", "dog food").eq("category", "dogs"),
  )
  .take(10);

Pagination

import { paginationOptsValidator } from "convex/server";

export const list = query({
  args: { paginationOpts: paginationOptsValidator, category: v.string() },
  handler: async (ctx, args) => {
    return await ctx.db
      .query("products")
      .withIndex("by_category", (q) => q.eq("category", args.category))
      .order("desc")
      .paginate(args.paginationOpts);
  },
});
// Returns: { page, isDone, continueCursor }

10. Mutations

  • ctx.db.patch(id, fields) — shallow merge; throws if document does not exist
  • ctx.db.replace(id, doc) — full replace; throws if document does not exist
  • ctx.db.insert(table, doc) — insert new document, returns Id
  • ctx.db.delete(id) — delete document by id

11. Actions

  • Add "use node"; at the top of files whose actions use Node.js built-ins
  • Never put "use node"; in a file that also exports queries or mutations — separate the action into its own file
  • fetch() is available in the default Convex (V8) runtime — no "use node" needed just for fetch
  • Never use ctx.db inside an action — actions have no database access; use ctx.runQuery / ctx.runMutation instead
  • Only call an action from another action when you need to cross runtimes; otherwise extract shared logic into a plain async helper
"use node";
import { internalAction } from "./_generated/server";

export const myAction = internalAction({
  args: {},
  handler: async (ctx, args) => {
    // Node.js built-ins available here
    return null;
  },
});

12. Scheduling & Crons

  • Use only crons.interval or crons.cron — do NOT use crons.hourly, crons.daily, or crons.weekly
  • Both methods take a FunctionReference — never pass the function directly
  • Export crons as default from convex/crons.ts
  • Always import internal from _generated/api when a cron calls an internal function, even in the same file
import { cronJobs } from "convex/server";
import { internal } from "./_generated/api";

const crons = cronJobs();

crons.interval("cleanup expired carts", { hours: 24 }, internal.carts.cleanupExpired, {});

export default crons;

13. File Storage

  • ctx.storage.getUrl(storageId) — returns a signed URL or null if the file doesn't exist
  • Do not use the deprecated ctx.storage.getMetadata() — query the _storage system table instead:
const metadata = await ctx.db.system.get("_storage", args.fileId);
  • Storage items are Blob objects — convert to/from Blob when reading or writing

14. API Design & File Organisation

  • Use file-based routing to organise public functions logically: convex/products.ts, convex/orders.ts, etc.
  • Extract shared business logic into convex/model/*.ts helpers and import them into public function files
  • Keep internalQuery/internalMutation/internalAction in the same file as the public function that calls them, unless the file would exceed a manageable size
  • Avoid chaining many ctx.runQuery/ctx.runMutation calls from a single action — race conditions become likely