Leave Localhost logoLeave LocalhostDocs
Recipes

Add a New Convex Table

Add a table to the Convex schema with indexes, validators, and the queries and mutations that read and write it.

Add a new table to the Convex backend (packages/backend/convex). Read the Convex Backend overview and the project's Convex guidelines first; the rules there override generic Convex advice.

1. Define the table in the schema

Add it to defineSchema in packages/backend/convex/schema.ts, with the indexes you will query by:

reports: defineTable({
  organizationId: v.string(),
  title: v.string(),
  createdBy: v.string(),
}).index("by_organization", ["organizationId"]),

Always query by an index rather than filtering full scans. The Data Model page describes the existing tables and conventions (app-owned *_profiles vs. Better Auth tables).

2. Add queries and mutations

Create a module (e.g. convex/reports/queries.ts and mutations.ts) using the new function syntax with explicit args and returns validators:

export const listForOrganization = query({
  args: { organizationId: v.string() },
  returns: v.array(/* ... */),
  handler: async (ctx, args) => {
    return await ctx.db
      .query("reports")
      .withIndex("by_organization", (q) =>
        q.eq("organizationId", args.organizationId),
      )
      .collect();
  },
});

3. Authorize every function

Never trust the client for identity or membership. Resolve the actor and check a permission with requireAppPermission before reading or writing org-scoped data:

const actor = await requireAppPermission(ctx, { permission: "organization.read" });
// use actor.organizationId, not an id from args

4. Audit writes that matter

For security-, billing-, or admin-relevant changes, write an audit row in the same transaction — see Write an Audit Event.

After editing the schema, the Convex dev server regenerates types in convex/_generated. Do not edit generated files by hand.

On this page