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,.relations(...),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
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 'kitcn/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
import {
boolean,
convexTable,
defineSchema,
id,
index,
integer,
text,
} from 'kitcn/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)]);
const tables = { users, posts };
export default defineSchema(tables).relations((r) => ({
users: {
posts: r.many.posts(),
},
posts: {
author: r.one.users({ from: r.posts.userId, to: r.users.id }),
},
}));Query published posts with their authors:
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 },
});
});