Overview
Drizzle-style relational queries for Convex with full type safety
The ORM mirrors Drizzle's relational query builder while adapting to Convex's document model.
What You Get
- Drizzle-style schema and relations (
convexTable,defineRelations,one,many) - Type-safe queries (
findMany,findFirst, cursor pagination,where,orderBy,with) - Mutations (
insert,update,delete,returning,onConflictDoUpdate) - Runtime RLS policies (
rlsPolicy,convexTable.withRLS) - End-to-end TypeScript inference (
InferSelectModel,$inferSelect)
ctx.orm is attached once during setup (see Quickstart). All examples below assume ctx.orm is available.
Where It Differs From SQL ORMs
Here's a quick summary of the key differences you'll encounter coming from a SQL-based ORM:
idandcreatedAtare auto-created by Convex- System
createdAtisnumber; definecreatedAt: timestamp().notNull().defaultNow()forDate timestamp()is the recommended point-in-time type for audit fields likecreatedAt/updatedAtdate()is calendar-only (YYYY-MM-DD) and should be used for date-without-time fields- Fields are nullable by default (
text()->string | null) columnsprojection and string operators run post-fetchwithis relation loading (no SQL joins)- Cursor pagination needs an index on the primary
orderByfield
Timestamps And Dates
The ORM keeps Convex storage constraints and gives you a stronger public type API:
- Public system fields are
idandcreatedAtin ORM APIs. - Internal Convex fields (
_id,_creationTime) stay internal-only in ORM APIs. timestamp()gives point-in-time semantics and returnsDateby default.date()gives calendar-date semantics and can returnstringorDatedepending on mode.
For most tables, define an explicit timestamp column:
import { convexTable, text, timestamp } from 'better-convex/orm';
export const posts = convexTable('posts', {
title: text().notNull(),
createdAt: timestamp().notNull().defaultNow(), // Date in ORM API
});With cRPC, Date wire serialization is built in, so Date values round-trip without manual getTime() calls.
Quick Example
Let's define a simple schema with users and posts tables, then wire up relations between them:
import { convexTable, defineRelations, text, boolean, integer, id, index } from 'better-convex/orm';
export const users = convexTable('users', {
name: text().notNull(),
email: text().notNull(),
role: text(),
age: integer(),
});
export const posts = convexTable('posts', {
title: text().notNull(),
content: text().notNull(),
published: boolean(),
userId: id('users'),
}, (t) => [index('by_published').on(t.published)]);
export const relations = defineRelations({ users, posts }, (r) => ({
users: { posts: r.many.posts() },
posts: {
author: r.one.users({ from: r.posts.userId, to: r.users.id }),
},
}));Here's how to query published posts with their authors using ctx.orm in cRPC:
import { publicQuery } from '../lib/crpc';
export const getRecentPosts = publicQuery.query(async ({ ctx }) => {
return ctx.orm.query.posts.findMany({
where: { published: true },
orderBy: { createdAt: 'desc' },
limit: 10,
with: { author: true },
});
});You now have a fully typed query with relation loading in just a few lines.
Next Steps
Quickstart
Get running in 5 minutes
Migrate from native Convex
ctx.db to ORM mapping guide
Migrate from Convex Ents
Ents to ORM mapping guide
Schema Definition
Tables, relations, and indexes
Querying Data
Filters, ordering, and pagination
Pagination
Offset vs cursor pagination reference
Row-Level Security
rlsPolicy rules and enforcement
Triggers
Automatic side effects in schema
Drizzle Comparison
Side-by-side API mapping