# Coding Guidelines - Convex Backend - `convex/` Convex serverless backend - functions, schema, real-time database --- ## Patterns & Conventions ### Schema - ✅ DO: Define tables in `schema.ts` like the existing pattern - ✅ DO: Add indexes for frequently queried fields ```typescript // schema.ts pattern export default defineSchema({ tasks: defineTable({ text: v.string(), isCompleted: v.boolean(), }).index('by_completion', ['isCompleted']), }) ``` ### Query Functions - ✅ DO: Use `query()` for read operations like `quickstart_tasks.ts` ```typescript export const list = query({ args: {}, handler: async (ctx) => { return await ctx.db.query('tasks').collect() }, }) ``` ### Mutation Functions - ✅ DO: Use `mutation()` for write operations ```typescript export const create = mutation({ args: { text: v.string() }, handler: async (ctx, args) => { return await ctx.db.insert('tasks', { text: args.text, isCompleted: false }) }, }) ``` ### Actions (External APIs) - ✅ DO: Use `action()` for external HTTP calls - ❌ DON'T: Access `ctx.db` directly in actions (use `ctx.runMutation`) --- ## Key Files | Purpose | File | |---------|------| | Schema | `schema.ts` | | Custom types | `types.ts` | --- ## Frontend Usage ```typescript // In React components import { useQuery, useMutation } from 'convex/react' import { api } from '@convex/_generated/api' const tasks = useQuery(api.quickstart_tasks.get) const addTask = useMutation(api.quickstart_tasks.add) ``` --- ## Function Guidelines ### New Function Syntax - ALWAYS use the new function syntax: `query({ args: {...}, returns: {...}, handler: async (ctx, args) => {...} })` - Import `query`, `mutation`, or `action` from `./_generated/server` - Import validators from `convex/values` using `v` - Structure: define `args`, `returns`, and `handler` as object properties ### HTTP Endpoint Syntax - HTTP endpoints are defined in `convex/http.ts` and require an `httpAction` decorator - Use `httpRouter()` to create a router, then `http.route()` with `path`, `method`, and `handler` properties - HTTP endpoints are always registered at the exact path you specify in the `path` field ### Function Registration - Use `internalQuery`, `internalMutation`, and `internalAction` to register internal functions. These functions are private and aren't part of an app's API. They can only be called by other Convex functions. These functions are always imported from `./_generated/server`. - Use `query`, `mutation`, and `action` to register public functions. These functions are part of the public API and are exposed to the public Internet. Do NOT use `query`, `mutation`, or `action` to register sensitive internal functions that should be kept private. - You CANNOT register a function through the `api` or `internal` objects. - ALWAYS include argument and return validators for all Convex functions. This includes all of `query`, `internalQuery`, `mutation`, `internalMutation`, `action`, and `internalAction`. If a function doesn't return anything, include `returns: v.null()` as its output validator. - If the JavaScript implementation of a Convex function doesn't have a return value, it implicitly returns `null`. ### Function Calling - Use `ctx.runQuery` to call a query from a query, mutation, or action. - Use `ctx.runMutation` to call a mutation from a mutation or action. - Use `ctx.runAction` to call an action from an action. - ONLY call an action from another action if you need to cross runtimes (e.g. from V8 to Node). Otherwise, pull out the shared code into a helper async function and call that directly instead. - Try to use as few calls from actions to queries and mutations as possible. Queries and mutations are transactions, so splitting logic up into multiple calls introduces the risk of race conditions. - All of these calls take in a `FunctionReference`. Do NOT try to pass the callee function directly into one of these calls. - When using `ctx.runQuery`, `ctx.runMutation`, or `ctx.runAction` to call a function in the same file, specify a type annotation on the return value to work around TypeScript circularity limitations (e.g., `const result: string = await ctx.runQuery(api.example.f, {...})`) ### Function References - Function references are pointers to registered Convex functions. - Use the `api` object defined by the framework in `convex/_generated/api.ts` to call public functions registered with `query`, `mutation`, or `action`. - Use the `internal` object defined by the framework in `convex/_generated/api.ts` to call internal (or private) functions registered with `internalQuery`, `internalMutation`, or `internalAction`. - Convex uses file-based routing, so a public function defined in `convex/example.ts` named `f` has a function reference of `api.example.f`. - A private function defined in `convex/example.ts` named `g` has a function reference of `internal.example.g`. - Functions can also registered within directories nested within the `convex/` folder. For example, a public function `h` defined in `convex/messages/access.ts` has a function reference of `api.messages.access.h`. ### API Design - Convex uses file-based routing, so thoughtfully organize files with public query, mutation, or action functions within the `convex/` directory. - Use `query`, `mutation`, and `action` to define public functions. - Use `internalQuery`, `internalMutation`, and `internalAction` to define private, internal functions. ### Pagination - Paginated queries return a list of results in incremental pages - Import `paginationOptsValidator` from `convex/server` and use it in your query args - Use `.paginate(args.paginationOpts)` on a query to enable pagination - `paginationOpts` contains: `numItems` (max documents to return) and `cursor` (for next page) - Returns an object with: `page` (array of documents), `isDone` (boolean), and `continueCursor` (string for next page) --- ## Validator Guidelines - `v.bigint()` is deprecated for representing signed 64-bit integers. Use `v.int64()` instead. - Use `v.record()` for defining a record type. `v.map()` and `v.set()` are not supported. - Always use the `v.null()` validator when returning a null value or when a function doesn't return anything. - Use `v.array()` for array types: `v.array(v.union(v.string(), v.number()))` - Use `v.union()` with `v.literal()` for discriminated union types and `as const` for string literals - Use `v.object()` for object types with specific properties - Use `v.optional()` for optional fields ## Validators Table Here are the valid Convex types along with their respective validators: | Convex Type | TS/JS type | Example usage | Validator for argument validation and schemas | Notes | | ----------- | ---------- | ------------- | --------------------------------------------- | ----- | | Id | string | `doc._id` | `v.id(tableName)` | | | Null | null | `null` | `v.null()` | JavaScript's `undefined` is not a valid Convex value. Functions that return `undefined` or do not return will return `null` when called from a client. Use `null` instead. | | Int64 | bigint | `3n` | `v.int64()` | Int64s only support BigInts between -2^63 and 2^63-1. Convex supports `bigint`s in most modern browsers. | | Float64 | number | `3.1` | `v.number()` | Convex supports all IEEE-754 double-precision floating point numbers (such as NaNs). Inf and NaN are JSON serialized as strings. | | Boolean | boolean | `true` | `v.boolean()` | | | String | string | `"abc"` | `v.string()` | Strings are stored as UTF-8 and must be valid Unicode sequences. Strings must be smaller than the 1MB total size limit when encoded as UTF-8. | | Bytes | ArrayBuffer | `new ArrayBuffer(8)` | `v.bytes()` | Convex supports first class bytestrings, passed in as `ArrayBuffer`s. Bytestrings must be smaller than the 1MB total size limit for Convex types. | | Array | Array | `[1, 3.2, "abc"]` | `v.array(values)` | Arrays can have at most 8192 values. | | Object | Object | `{a: "abc"}` | `v.object({property: value})` | Convex only supports "plain old JavaScript objects" (objects that do not have a custom prototype). Objects can have at most 1024 entries. Field names must be nonempty and not start with "$" or "_". | | Record | Record | `{"a": "1", "b": "2"}` | `v.record(keys, values)` | Records are objects at runtime, but can have dynamic keys. Keys must be only ASCII characters, nonempty, and not start with "$" or "\_". | ## Validator Builder (https://docs.convex.dev/api/modules/values#v) Validators can be used in schema definitions and as input validators for Convex functions. This builder allows you to build validators for Convex values. | Name | Type | | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | \(`tableName`: `TableName`) => [`VId`](/api/classes/values.VId.md)<[`GenericId`](/api/modules/values.md#genericid)<`TableName`>, `"required"`> | | `null` | () => [`VNull`](/api/classes/values.VNull.md)<`null`, `"required"`> | | `number` | () => [`VFloat64`](/api/classes/values.VFloat64.md)<`number`, `"required"`> | | `float64` | () => [`VFloat64`](/api/classes/values.VFloat64.md)<`number`, `"required"`> | | `bigint` | () => [`VInt64`](/api/classes/values.VInt64.md)<`bigint`, `"required"`> | | `int64` | () => [`VInt64`](/api/classes/values.VInt64.md)<`bigint`, `"required"`> | | `boolean` | () => [`VBoolean`](/api/classes/values.VBoolean.md)<`boolean`, `"required"`> | | `string` | () => [`VString`](/api/classes/values.VString.md)<`string`, `"required"`> | | `bytes` | () => [`VBytes`](/api/classes/values.VBytes.md)<`ArrayBuffer`, `"required"`> | | `literal` | \(`literal`: `T`) => [`VLiteral`](/api/classes/values.VLiteral.md)<`T`, `"required"`> | | `array` | \(`element`: `T`) => [`VArray`](/api/classes/values.VArray.md)<`T`\[`"type"`]\[], `T`, `"required"`> | | `object` | \(`fields`: `T`) => [`VObject`](/api/classes/values.VObject.md)<[`Expand`](/api/modules/server.md#expand)<{ \[Property in string \| number \| symbol]?: Exclude\, undefined> } & { \[Property in string \| number \| symbol]: Infer\ }>, `T`, `"required"`, { \[Property in string \| number \| symbol]: Property \| \`${Property & string}.${T\[Property]\["fieldPaths"]}\` }\[keyof `T`] & `string`> | | `record` | \(`keys`: `Key`, `values`: `Value`) => [`VRecord`](/api/classes/values.VRecord.md)<`Record`<[`Infer`](/api/modules/values.md#infer)<`Key`>, `Value`\[`"type"`]>, `Key`, `Value`, `"required"`, `string`> | | `union` | \(...`members`: `T`) => [`VUnion`](/api/classes/values.VUnion.md)<`T`\[`number`]\[`"type"`], `T`, `"required"`, `T`\[`number`]\[`"fieldPaths"`]> | | `any` | () => [`VAny`](/api/classes/values.VAny.md)<`any`, `"required"`, `string`> | | `optional` | \(`value`: `T`) => [`VOptional`](/api/modules/values.md#voptional)<`T`> | | `nullable` | \(`value`: `T`) => [`VUnion`](/api/classes/values.VUnion.md)<`T` \| [`VNull`](/api/classes/values.VNull.md)<`null`, `"required"`>\[`"type"`], \[`T`, [`VNull`](/api/classes/values.VNull.md)<`null`, `"required"`>], `"required"`, `T` \| [`VNull`](/api/classes/values.VNull.md)<`null`, `"required"`>\[`"fieldPaths"`]> | --- ## Schema Guidelines - Always define your schema in `convex/schema.ts`. - Always import the schema definition functions from `convex/server`: - System fields are automatically added to all documents and are prefixed with an underscore. The two system fields that are automatically added to all documents are `_creationTime` which has the validator `v.number()` and `_id` which has the validator `v.id(tableName)`. - Always include all index fields in the index name. For example, if an index is defined as `["field1", "field2"]`, the index name should be "by_field1_and_field2". - Index fields must be queried in the same order they are defined. If you want to be able to query by "field1" then "field2" and by "field2" then "field1", you must create separate indexes. --- ## TypeScript Guidelines - Use the `Id<'tableName'>` type from `./_generated/dataModel` for document IDs - For `Record` types with ID keys, specify both key and value types: `Record, string>` for validator `v.record(v.id('users'), v.string())` - Be strict with types, particularly around document IDs - use `Id<'users'>` rather than `string` - Always use `as const` for string literals in discriminated union types - Define arrays as `const array: Array = [...]` and records as `const record: Record = {...}` - Add `@types/node` to `package.json` when using Node.js built-in modules --- ## Full Text Search Guidelines - Use `.withSearchIndex()` with `.search()` and `.eq()` for filtered full-text searches - Example pattern: `ctx.db.query("messages").withSearchIndex("search_body", (q) => q.search("body", "search terms").eq("channel", "#general")).take(10)` --- ## Query Guidelines - Do NOT use `filter` in queries. Instead, define an index in the schema and use `withIndex` instead. - Convex queries do NOT support `.delete()`. Instead, `.collect()` the results, iterate over them, and call `ctx.db.delete(row._id)` on each result. - Use `.unique()` to get a single document from a query. This method will throw an error if there are multiple documents that match the query. - When using async iteration, don't use `.collect()` or `.take(n)` on the result of a query. Instead, use the `for await (const row of query)` syntax. ### Ordering - By default Convex always returns documents in ascending `_creationTime` order. - You can use `.order('asc')` or `.order('desc')` to pick whether a query is in ascending or descending order. If the order isn't specified, it defaults to ascending. - Document queries that use indexes will be ordered based on the columns in the index and can avoid slow table scans. --- ## Mutation Guidelines - Use `ctx.db.replace` to fully replace an existing document. This method will throw an error if the document does not exist. - Use `ctx.db.patch` to shallow merge updates into an existing document. This method will throw an error if the document does not exist. --- ## Action Guidelines - Always add `"use node";` to the top of files containing actions that use Node.js built-in modules - Never use `ctx.db` inside of an action - actions don't have access to the database - Actions follow the same syntax structure: `action({ args: {...}, returns: {...}, handler: async (ctx, args) => {...} })` --- ## Scheduling Guidelines ### Cron Guidelines - Only use `crons.interval` or `crons.cron` methods to schedule cron jobs (NOT `crons.hourly`, `crons.daily`, or `crons.weekly`) - Both methods take a FunctionReference - do NOT pass the function directly - Define crons by declaring `crons` object, calling methods on it, then exporting as default - Pattern: `crons.interval('job name', { hours: 2 }, internal.file.functionName, {})` - You can register Convex functions within `crons.ts` just like any other file - Always import `internal` from `_generated/api`, even for functions in the same file --- ## File Storage Guidelines - Convex includes file storage for large files like images, videos, and PDFs - `ctx.storage.getUrl()` returns a signed URL for a file (returns `null` if file doesn't exist) - Do NOT use deprecated `ctx.storage.getMetadata` - instead query the `_storage` system table with `ctx.db.system.get(fileId)` - File metadata includes: `_id`, `_creationTime`, `contentType`, `sha256`, and `size` - Convex storage stores items as `Blob` objects - convert items to/from `Blob` when using storage --- ## Examples ### Example Schema A well-crafted schema demonstrates: - Simple tables with basic field types (`users` with `name: v.string()`) - Tables with foreign key references (`sessions` with `userId: v.id('users')`) - Indexes for efficient queries (`index('sessionId', ['sessionId'])`) - Optional fields (`summary: v.optional(v.string())`) - References to system tables (`summarizer: v.optional(v.id('_scheduled_functions'))`) - Complex discriminated unions with `v.union()` and `v.literal()` for the `author` field with different roles (system, assistant, user) - Multiple indexes per table when needed (`threads` indexed by `uuid`, `messages` indexed by `threadId`) --- ## Common Gotchas - Types outdated → Run `npx convex dev` - Function not found → Check export and file location - Actions can't access `ctx.db` → Use `ctx.runMutation/runQuery` - Index not working → Define in `schema.ts`