BETTER-CONVEX

Insert

Insert rows with Drizzle-style builders

In this guide, we'll learn how to insert rows into your Convex tables using the ORM's Drizzle-style insert() builder. You'll see single inserts, multi-row inserts, returning clauses, and upsert patterns.

Basic Insert

Let's start with a simple mutation that inserts a single user:

convex/functions/users.ts
import { z } from 'zod';
import { publicMutation } from '../lib/crpc';
import { users } from '../schema';

export const createUser = publicMutation
  .input(z.object({ email: z.string().email(), name: z.string() }))
  .mutation(async ({ ctx, input }) => {
    await ctx.orm.insert(users).values({
      email: input.email,
      name: input.name,
    });
  });

If you need the insert type for a table, you can infer it from the table definition:

type NewUser = typeof users.$inferInsert;

Returning

By default, insert() returns void. Use .returning() to get the inserted rows back.

You can return all fields or pick specific columns:

const [user] = await ctx.orm
  .insert(users)
  .values({ email: 'ada@example.com', name: 'Ada' })
  .returning();

const [partial] = await ctx.orm
  .insert(users)
  .values({ email: 'grace@example.com', name: 'Grace' })
  .returning({ id: users.id, email: users.email });

Note: .returning() always returns an array, even for single-row inserts.

Insert Multiple Rows

You can pass an array to .values() to insert several rows at once:

await ctx.orm.insert(users).values([
  { email: 'a@example.com', name: 'A' },
  { email: 'b@example.com', name: 'B' },
]);

Upserts and Conflicts

The ORM mirrors Drizzle's onConflictDoNothing() and onConflictDoUpdate() surface, enforced at runtime against your table's unique constraints and indexes.

On Conflict Do Nothing

Use onConflictDoNothing to silently skip rows that would violate a unique constraint:

await ctx.orm
  .insert(users)
  .values({ email: 'ada@example.com', name: 'Ada' })
  .onConflictDoNothing({ target: users.email });

You can also omit target to skip inserts on any unique conflict:

await ctx.orm
  .insert(users)
  .values({ email: 'ada@example.com', name: 'Ada' })
  .onConflictDoNothing();

On Conflict Do Update

Use onConflictDoUpdate to update the existing row when a conflict occurs:

await ctx.orm
  .insert(users)
  .values({ email: 'ada@example.com', name: 'Ada' })
  .onConflictDoUpdate({
    target: users.email,
    set: { name: 'Ada Lovelace' },
  });

Important: onConflictDoUpdate() requires a target column and a backing unique constraint or index (uniqueIndex(), .unique(), unique()).

Constraint and RLS Enforcement

The ORM enforces unique constraints, foreign keys, and RLS policies at runtime for all insert and upsert operations.

Note: Direct ctx.db.insert(...) bypasses these checks. Use ctx.orm to get full constraint enforcement.

That's it. You now have everything you need to insert data - from simple single-row inserts to conflict-handling upserts.

Next Steps

On this page