OrmAI

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.

Dipankar Sarkar · ·Updated April 15, 2026 prismatypescriptnextjs

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 $queryRaw for 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 select masking).
  • Audit log is automatic.
  • The shape of where is 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.


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