Files
DiscordClone/CONVEX_RULES.md

7.5 KiB

Convex Framework Guidelines

Generic Convex best practices for building backend applications.


Function Guidelines

New Function Syntax

ALWAYS use the new function syntax for Convex functions:

import { query } from "./_generated/server";
import { v } from "convex/values";
export const f = query({
    args: {},
    returns: v.null(),
    handler: async (ctx, args) => {
    // Function body
    },
});

HTTP Endpoint Syntax

HTTP endpoints are defined in convex/http.ts and require an httpAction decorator:

import { httpRouter } from "convex/server";
import { httpAction } from "./_generated/server";
const http = httpRouter();
http.route({
    path: "/echo",
    method: "POST",
    handler: httpAction(async (ctx, req) => {
    const body = await req.bytes();
    return new Response(body, { status: 200 });
    }),
});

HTTP endpoints are registered at the exact path you specify.

Validators

Array validator example:

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

export default mutation({
args: {
    simpleArray: v.array(v.union(v.string(), v.number())),
},
handler: async (ctx, args) => {
    //...
},
});

Discriminated union example:

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

export default defineSchema({
    results: defineTable(
        v.union(
            v.object({
                kind: v.literal("error"),
                errorMessage: v.string(),
            }),
            v.object({
                kind: v.literal("success"),
                value: v.number(),
            }),
        ),
    )
});

Always use v.null() when returning null:

export const exampleQuery = query({
  args: {},
  returns: v.null(),
  handler: async (ctx, args) => {
      return null;
  },
});

Convex Types Reference

Convex Type TS/JS type Example Validator Notes
Id string doc._id v.id(tableName)
Null null null v.null() Use null instead of undefined
Int64 bigint 3n v.int64() -2^63 to 2^63-1
Float64 number 3.1 v.number() IEEE-754 double-precision
Boolean boolean true v.boolean()
String string "abc" v.string() UTF-8, <1MB
Bytes ArrayBuffer new ArrayBuffer(8) v.bytes() <1MB
Array Array [1, 3.2] v.array(values) Max 8192 values
Object Object {a: "abc"} v.object({property: value}) Max 1024 entries
Record Record {"a": "1"} v.record(keys, values) ASCII keys only

Function Registration

  • Use internalQuery, internalMutation, internalAction for private functions (only callable by other Convex functions)
  • Use query, mutation, action for public API functions
  • You CANNOT register functions through the api or internal objects
  • ALWAYS include argument and return validators. Use returns: v.null() if no return value

Function Calling

  • ctx.runQuery - call a query from query, mutation, or action
  • ctx.runMutation - call a mutation from mutation or action
  • ctx.runAction - call an action from action (only for crossing runtimes V8 to Node)
  • All calls take a FunctionReference, not the function directly
  • Minimize calls from actions to queries/mutations (risk of race conditions)

When calling functions in the same file, add type annotation:

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

Function References

  • Use api object for public functions: api.example.f
  • Use internal object for private functions: internal.example.g
  • File-based routing: convex/messages/access.ts -> api.messages.access.h

Validator Guidelines

  • v.bigint() is deprecated - use v.int64() instead
  • Use v.record() for record types. v.map() and v.set() are not supported

Schema Guidelines

  • Define schema in convex/schema.ts
  • Import schema functions from convex/server
  • System fields _creationTime (v.number()) and _id (v.id(tableName)) are automatic
  • Name indexes after their fields: ["field1", "field2"] -> "by_field1_and_field2"
  • Index fields must be queried in order defined

TypeScript Guidelines

  • Use Id<'tableName'> from ./_generated/dataModel for typed IDs
  • Record example: Record<Id<"users">, string>
  • Use as const for string literals in discriminated unions
  • Always define arrays as const array: Array<T> = [...]
  • Always define records as const record: Record<K, V> = {...}
  • Add @types/node when using Node.js built-in modules

Query Guidelines

  • Do NOT use filter - define an index and use withIndex instead
  • No .delete() - use .collect() then iterate with ctx.db.delete(row._id)
  • Use .unique() for single document (throws if multiple match)
  • For async iteration, use for await (const row of query) instead of .collect()

Ordering

  • Default: ascending _creationTime
  • Use .order('asc') or .order('desc')
  • Indexed queries ordered by index columns
const messages = await ctx.db
  .query("messages")
  .withSearchIndex("search_body", (q) =>
    q.search("body", "hello hi").eq("channel", "#general"),
  )
  .take(10);

Mutation Guidelines

  • ctx.db.replace - fully replace document (throws if not exists)
  • ctx.db.patch - shallow merge updates (throws if not exists)

Action Guidelines

  • Add "use node"; at top of files using Node.js modules
  • Actions don't have database access (ctx.db not available)
import { action } from "./_generated/server";

export const exampleAction = action({
    args: {},
    returns: v.null(),
    handler: async (ctx, args) => {
        return null;
    },
});

Pagination

import { paginationOptsValidator } from "convex/server";

export const listWithExtraArg = query({
    args: { paginationOpts: paginationOptsValidator, author: v.string() },
    handler: async (ctx, args) => {
        return await ctx.db
        .query("messages")
        .withIndex("by_author", (q) => q.eq("author", args.author))
        .order("desc")
        .paginate(args.paginationOpts);
    },
});

paginationOpts: { numItems: number, cursor: string | null }

Returns: { page: Doc[], isDone: boolean, continueCursor: string }


Cron Jobs

  • Use crons.interval or crons.cron only (not hourly/daily/weekly helpers)
  • Pass FunctionReference, not the function directly
import { cronJobs } from "convex/server";
import { internal } from "./_generated/api";

const crons = cronJobs();
crons.interval("job name", { hours: 2 }, internal.crons.myFunction, {});
export default crons;

File Storage

  • ctx.storage.getUrl() returns signed URL (null if file doesn't exist)
  • Query _storage system table for metadata (don't use deprecated ctx.storage.getMetadata)
const metadata = await ctx.db.system.get(args.fileId);
// Returns: { _id, _creationTime, contentType?, sha256, size }
  • Store items as Blob objects