# 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`: ```typescript 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`: ```typescript 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: ```typescript 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.ts` → `api.products.myFn`; `convex/model/users.ts` → `api.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 = [...]` - Declare records as `const rec: Record = {...}` --- ## 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 ### Full-text search ```typescript const results = await ctx.db .query("products") .withSearchIndex("search_name", (q) => q.search("name", "dog food").eq("category", "dogs"), ) .take(10); ``` ### Pagination ```typescript 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 ```typescript "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 ```typescript 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: ```typescript 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