OrmAI

Guide

OrmAI with Drizzle

Drizzle is the ORM of choice for teams who want type safety without code generation. Here's how to wrap it with OrmAI's policy engine.

Dipankar Sarkar · ·Updated April 15, 2026 drizzletypescriptedge

Drizzle has become the default for many TypeScript projects: zero code generation, edge-friendly, SQL-first. OrmAI’s @ormai/drizzle adapter wraps a Drizzle database with the same policy engine as the Python and Prisma versions.

Install

pnpm add @ormai/core @ormai/drizzle

Wire it up

Assume you have a Drizzle schema:

// db/schema.ts
import { pgTable, integer, varchar, timestamp, serial } from "drizzle-orm/pg-core";

export const customers = pgTable("customers", {
  id: serial("id").primaryKey(),
  tenantId: integer("tenant_id").notNull(),
  name: varchar("name", { length: 120 }).notNull(),
  email: varchar("email", { length: 120 }).notNull(),
  apiSecret: varchar("api_secret", { length: 64 }),
});

export const orders = pgTable("orders", {
  id: serial("id").primaryKey(),
  tenantId: integer("tenant_id").notNull(),
  customerId: integer("customer_id").notNull().references(() => customers.id),
  totalCents: integer("total_cents").notNull(),
  status: varchar("status", { length: 20 }).default("pending").notNull(),
  createdAt: timestamp("created_at").defaultNow().notNull(),
});

Wire the adapter:

// lib/ormai.ts
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
import { mountDrizzle, PolicyBuilder, DEFAULT_PROD } from "@ormai/core";
import * as schema from "@/db/schema";

const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export const db = drizzle(pool, { schema });

export const policy = new PolicyBuilder(DEFAULT_PROD)
  .registerSchema(schema, ["customers", "orders"])
  .denyFields(["*secret*", "*password*", "*token*"])
  .maskFields(["customers.email"])
  .tenantScope("tenantId")
  .enableWrites(["orders"], { requireReason: true })
  .maxRows(200)
  .build();

export const toolset = mountDrizzle({ db, schema, policy });

registerSchema introspects the Drizzle schema object and registers the named tables. tenantScope("tenantId") matches the camelCase JS property — OrmAI knows it maps to tenant_id in SQL.

Edge runtime

Drizzle is the easiest ORM to run on Edge. With @neondatabase/serverless:

import { drizzle } from "drizzle-orm/neon-http";
import { neon } from "@neondatabase/serverless";

const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql, { schema });

The OrmAI adapter doesn’t care which driver is underneath. It calls Drizzle’s query builder.

Type safety

Drizzle’s types are exceptional, and OrmAI preserves them. The model parameter in db.query is constrained to the table names you registered:

toolset.execute("db.query", { model: "orders" }); // ok
toolset.execute("db.query", { model: "OrderItems" }); // ts: not in registry

Returned rows are typed against the Drizzle schema, with redacted fields typed as string (the masked value) or null (denied).

Domain tools with Drizzle

Same pattern as Prisma — register a tool() that uses the Drizzle client directly:

import { tool } from "@ormai/core";
import { db } from "@/lib/db";
import { orders } from "@/db/schema";
import { and, eq, gte, sql } from "drizzle-orm";

export const weeklyRevenue = tool({
  name: "analytics.weekly_revenue",
  description: "Sum of paid orders this calendar week.",
  input: {},
  async execute(_, ctx) {
    const result = await db
      .select({ cents: sql<number>`SUM(${orders.totalCents})` })
      .from(orders)
      .where(and(
        eq(orders.tenantId, ctx.tenantId),
        eq(orders.status, "paid"),
        gte(orders.createdAt, weekStart()),
      ));
    return { cents: result[0]?.cents ?? 0 };
  },
});

The audit row is written automatically. Don’t forget to use ctx.tenantId — Drizzle doesn’t inject scoping for hand-written queries.

Drizzle-specific gotchas

db.execute() for raw SQL

Like Prisma’s $queryRaw, Drizzle’s db.execute(sql\…`)` is an escape hatch. Don’t expose it to the agent. Use it only in human-reviewed application code.

Column name mismatch

Drizzle separates the JS property name from the DB column name. OrmAI uses the JS name in policy declarations (tenantId), and translates to the DB name when generating SQL. If your schema has unusual aliases, double-check that policy field paths use the JS names.

Relational queries

Drizzle’s relational query builder (db.query.orders.findMany) is supported by OrmAI’s include parameter:

toolset.execute("db.query", {
  model: "orders",
  include: ["customer"],
  where: { status: "pending" },
});

Becomes a Drizzle relational query with the joined table scoped to the same tenant.

Migration from raw Drizzle

If your existing code:

const result = await db.select().from(orders).where(
  and(eq(orders.tenantId, session.tenantId), eq(orders.status, "pending")),
);

Becomes:

const result = await toolset.execute("db.query", {
  model: "orders",
  where: { status: "pending" },
}, ctx);

The tenant filter, the row cap, the audit row, and the field redaction all happen for free.


Found a typo or want to suggest a topic? Email [email protected].