Guide
OrmAI with Prisma
Wrap your Prisma client with OrmAI's policy engine. Field-level redaction, tenant scoping, and audit logs over Prisma — without changing your schema.
Prisma is the most common ORM in TypeScript. OrmAI’s @ormai/prisma adapter wraps a Prisma client and exposes it through OrmAI’s typed, policy-enforced toolset.
Install
pnpm add @ormai/core @ormai/prisma
You don’t need to change your schema. OrmAI introspects Prisma’s runtime model at build time.
Wire it up
// lib/ormai.ts
import { PrismaClient } from "@prisma/client";
import { mountPrisma, PolicyBuilder, DEFAULT_PROD } from "@ormai/core";
export const prisma = new PrismaClient();
export const policy = new PolicyBuilder(DEFAULT_PROD)
.registerModels(["Customer", "Order", "Subscription"])
.denyFields(["*password*", "*secret*", "*token*", "*apiKey*"])
.maskFields(["customer.email", "customer.phone"])
.tenantScope("tenantId") // your column name
.enableWrites(["Order"], { requireReason: true })
.maxRows(200)
.statementTimeoutMs(3000)
.maxWritesPerMinute(20)
.build();
export const toolset = mountPrisma({ prisma, policy });
What “registerModels” means
Prisma already knows your schema; OrmAI uses it. registerModels whitelists which models are visible to the agent. Anything not in the list is invisible — not denied with an error, invisible. The model can’t even see it in describe_schema.
This is the right default. Most production schemas have internal-only tables (queues, locks, migrations) that should never be reachable through an agent surface.
Tool calls in API routes
// app/api/agent/tool/route.ts
import { toolset } from "@/lib/ormai";
import { RunContext } from "@ormai/core";
import { auth } from "@/lib/auth";
export async function POST(req: Request) {
const { name, args } = await req.json();
const session = await auth();
if (!session) return new Response("unauthorized", { status: 401 });
const ctx = RunContext.create({
tenantId: session.tenantId,
userId: session.userId,
traceId: req.headers.get("x-trace-id") ?? crypto.randomUUID(),
});
const result = await toolset.execute(name, args, ctx);
if (!result.success) {
return Response.json(
{ error: result.error, policy: result.policyDecision },
{ status: 400 },
);
}
return Response.json({ data: result.data, auditId: result.auditId });
}
Type safety with the Prisma client
@ormai/prisma reuses Prisma’s generated types. So if you write your own domain tool that uses the Prisma client, it stays fully typed:
import { tool } from "@ormai/core";
import { prisma } from "@/lib/prisma";
export const churnCohort = tool({
name: "analytics.churn_cohort",
description: "Cohort retention for customers who signed up in a given month.",
input: { month: "string" },
async execute({ month }, ctx) {
return prisma.customer.findMany({
where: { tenantId: ctx.tenantId, createdAt: { gte: monthStart(month), lt: monthEnd(month) } },
select: { id: true, status: true },
});
},
});
OrmAI handles the audit row, the budget check, and the rate limit. You write the domain logic with normal Prisma. Note that ad-hoc domain tools should still respect ctx.tenantId — OrmAI cannot inject scoping into hand-written code that bypasses the generic db.* tools.
Prisma-specific gotchas
Generated query helpers
Prisma’s prisma.$queryRaw is a SQL escape hatch. OrmAI does not wrap this. If you expose $queryRaw to your agent (you shouldn’t), you lose all policy guarantees. We recommend:
- Build domain tools that use the structured Prisma client (
findMany,aggregate, etc.). - Reserve
$queryRawfor application code paths reviewed by humans.
Soft deletes
Prisma doesn’t have built-in soft delete. If you implement it via a deletedAt column, declare it as a default scope:
.modelDefaultWhere("Customer", { deletedAt: null })
Now every read of Customer excludes soft-deleted rows. The agent can’t see tombstones unless you explicitly add a tool for that.
Generated client + Edge runtime
Prisma’s accelerated driver and the Vercel data proxy are both fine. The standard prisma binary needs Node, so it won’t run on Edge — that’s a Prisma constraint, not an OrmAI constraint.
Migration from raw Prisma in your agent
If your existing agent code looks like:
const orders = await prisma.order.findMany({
where: { tenantId: session.tenantId, status: "pending" },
take: 50,
});
Migrate to:
const orders = await toolset.execute("db.query", {
model: "Order",
where: { status: "pending" },
limit: 50,
}, ctx);
Differences:
- Tenant scoping is automatic (don’t write it).
- Field redaction is automatic (you can drop your manual
selectmasking). - Audit log is automatic.
- The shape of
whereis OrmAI’s portable shape, not Prisma’s. (Mostly compatible; a few operators differ.)
The change is mechanical. We’ve automated it for several customers with a small codemod — happy to share.
Related
Found a typo or want to suggest a topic? Email [email protected].