Prisma Extensions add custom behavior to Prisma Client without modifying the schema — prisma.$extends({ model, query, result, client }) extends the client. The model component adds custom static methods: prisma.user.findManyActive(). The query component intercepts operations: query.user.findMany({ args, query }) can inject where: { deletedAt: null } for soft deletes. The result component adds computed fields to returned data without a database column. The client component adds top-level methods. Multiple $extends calls compose extensions, so you can stack soft-delete, multi-tenancy, and pagination extensions. Prisma.defineExtension creates shareable extension factories. Claude Code generates Prisma Extension factories for soft delete, multi-tenancy where-clause injection, computed fields, audit trail logging, and custom pagination helpers.
CLAUDE.md for Prisma Extensions
## Prisma Extensions Stack
- Version: prisma >= 5.14, @prisma/client >= 5.14
- Extend: const xprisma = prisma.$extends({ model, query, result, client })
- Soft delete: query component intercepts delete/findMany — injects deletedAt filter
- Computed: result component adds { name: { needs: { firstName }, compute: d => d.firstName } }
- Middleware: query.{ model }.{ operation } = async ({ args, query }) => { mutate args; return query(args) }
- Factory: Prisma.defineExtension — reusable named extension
- Stack: const db = prisma.$extends(softDelete).$extends(tenancy).$extends(pagination)
Soft Delete Extension
// lib/prisma/extensions/soft-delete.ts
import { Prisma } from "@prisma/client"
// Models that support soft delete — must have deletedAt: DateTime? field
type SoftDeletableModel = "order" | "customer" | "product"
const SOFT_DELETABLE: SoftDeletableModel[] = ["order", "customer", "product"]
export const softDeleteExtension = Prisma.defineExtension({
name: "soft-delete",
query: {
$allModels: {
// Intercept delete — set deletedAt instead of hard deleting
async delete({ model, args, query }) {
if (SOFT_DELETABLE.includes(model.toLowerCase() as SoftDeletableModel)) {
return (query as any)({
...args,
data: { deletedAt: new Date() },
} as Parameters<typeof query>[0])
}
return query(args)
},
// Intercept deleteMany
async deleteMany({ model, args, query }) {
if (SOFT_DELETABLE.includes(model.toLowerCase() as SoftDeletableModel)) {
return (query as any)({
...args,
data: { deletedAt: new Date() },
} as Parameters<typeof query>[0])
}
return query(args)
},
// Auto-exclude soft-deleted records from all reads
async findMany({ model, args, query }) {
if (SOFT_DELETABLE.includes(model.toLowerCase() as SoftDeletableModel)) {
args.where = { ...args.where, deletedAt: null }
}
return query(args)
},
async findFirst({ model, args, query }) {
if (SOFT_DELETABLE.includes(model.toLowerCase() as SoftDeletableModel)) {
args.where = { ...args.where, deletedAt: null }
}
return query(args)
},
async findUnique({ model, args, query }) {
if (SOFT_DELETABLE.includes(model.toLowerCase() as SoftDeletableModel)) {
// Convert to findFirst to allow adding where clause
return query(args)
}
return query(args)
},
async count({ model, args, query }) {
if (SOFT_DELETABLE.includes(model.toLowerCase() as SoftDeletableModel)) {
args.where = { ...args.where, deletedAt: null }
}
return query(args)
},
},
},
model: {
$allModels: {
// Hard delete for admin operations — bypasses soft delete
async hardDelete<T>(
this: T,
where: Parameters<Prisma.PrismaClientKnownRequestError["constructor"]>[0]
) {
const context = Prisma.getExtensionContext(this)
return (context as any).delete({ where })
},
// Restore a soft-deleted record
async restore<T>(this: T, where: object) {
const context = Prisma.getExtensionContext(this)
return (context as any).update({ where, data: { deletedAt: null } })
},
// Find including deleted
async findWithDeleted<T>(this: T, args: object = {}) {
const context = Prisma.getExtensionContext(this)
return (context as any).findMany(args)
},
},
},
})
Multi-Tenancy Extension
// lib/prisma/extensions/tenancy.ts — row-level tenant isolation
import { Prisma } from "@prisma/client"
import { AsyncLocalStorage } from "async_hooks"
// Tenant context storage — set this per-request
const tenantStorage = new AsyncLocalStorage<{ tenantId: string }>()
export function withTenant<T>(tenantId: string, fn: () => Promise<T>): Promise<T> {
return tenantStorage.run({ tenantId }, fn)
}
// Models with tenantId column
const TENANT_SCOPED = ["order", "customer", "product", "invoice"]
export const tenancyExtension = Prisma.defineExtension({
name: "tenancy",
query: {
$allModels: {
async $allOperations({ model, operation, args, query }) {
const store = tenantStorage.getStore()
if (!store) return query(args) // No tenant context — skip (e.g., background jobs)
if (!TENANT_SCOPED.includes(model.toLowerCase())) return query(args)
const { tenantId } = store
const mutatingOps = ["create", "createMany", "upsert"]
const readOps = ["findMany", "findFirst", "findUnique", "count", "aggregate", "groupBy"]
const writeOps = ["update", "updateMany", "delete", "deleteMany"]
if (mutatingOps.includes(operation)) {
// Inject tenantId on create
if (args.data) {
args = { ...args, data: { ...args.data, tenantId } }
}
} else if (readOps.includes(operation) || writeOps.includes(operation)) {
// Inject where clause for reads and updates
args = { ...args, where: { ...args.where, tenantId } }
}
return query(args)
},
},
},
})
Computed Fields Extension
// lib/prisma/extensions/computed-fields.ts — result transformations
import { Prisma } from "@prisma/client"
export const computedFieldsExtension = Prisma.defineExtension({
name: "computed-fields",
result: {
// Add fullName computed field to User model
user: {
fullName: {
needs: { firstName: true, lastName: true },
compute(user) {
return `${user.firstName} ${user.lastName}`.trim()
},
},
initials: {
needs: { firstName: true, lastName: true },
compute(user) {
const f = user.firstName?.[0] ?? ""
const l = user.lastName?.[0] ?? ""
return `${f}${l}`.toUpperCase()
},
},
},
// Add formatted total to Order model
order: {
totalDollars: {
needs: { totalCents: true },
compute(order) {
return (order.totalCents / 100).toFixed(2)
},
},
isOverdue: {
needs: { status: true, createdAt: true },
compute(order) {
if (order.status === "delivered" || order.status === "cancelled") return false
const slaHours = 48
const deadline = new Date(order.createdAt.getTime() + slaHours * 3600_000)
return new Date() > deadline
},
},
},
},
})
Pagination Extension
// lib/prisma/extensions/pagination.ts — cursor and offset pagination
import { Prisma } from "@prisma/client"
interface PaginateArgs {
page?: number
limit?: number
cursor?: string
}
export const paginationExtension = Prisma.defineExtension({
name: "pagination",
model: {
$allModels: {
async paginate<T, A>(
this: T,
args: Prisma.Exact<A, Prisma.Args<T, "findMany"> & PaginateArgs>
) {
const ctx = Prisma.getExtensionContext(this)
const { page = 1, limit = 20, cursor, ...findArgs } = args as any
if (cursor) {
// Cursor-based pagination (for infinite scroll)
const items = await (ctx as any).findMany({
...findArgs,
take: limit + 1,
cursor: { id: cursor },
skip: 1,
})
const hasMore = items.length > limit
return {
items: hasMore ? items.slice(0, limit) : items,
nextCursor: hasMore ? items[limit - 1]?.id : null,
hasMore,
}
}
// Offset-based pagination
const skip = (page - 1) * limit
const [items, total] = await Promise.all([
(ctx as any).findMany({ ...findArgs, take: limit, skip }),
(ctx as any).count({ where: findArgs.where }),
])
return {
items,
total,
page,
totalPages: Math.ceil(total / limit),
hasMore: skip + items.length < total,
}
},
},
},
})
Composing Extensions
// lib/prisma/client.ts — stack all extensions
import { PrismaClient } from "@prisma/client"
import { softDeleteExtension } from "./extensions/soft-delete"
import { tenancyExtension } from "./extensions/tenancy"
import { computedFieldsExtension } from "./extensions/computed-fields"
import { paginationExtension } from "./extensions/pagination"
const prismaBase = new PrismaClient({
log: process.env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
})
export const db = prismaBase
.$extends(softDeleteExtension)
.$extends(tenancyExtension)
.$extends(computedFieldsExtension)
.$extends(paginationExtension)
export type ExtendedPrismaClient = typeof db
// Usage
// const orders = await db.order.paginate({ page: 2, limit: 20, where: { status: "pending" } })
// const users = await db.user.findFirst({ ... }) // Returns .fullName computed field
// withTenant("tenant_123", async () => { await db.order.findMany() }) // Auto-filters by tenant
For the Drizzle ORM alternative when query builders, relations API, and migration tooling are preferred without Prisma’s layer of abstraction — Drizzle queries are closer to raw SQL with better TypeScript inference on complex joins, and migration files are plain SQL, see the Drizzle Advanced guide. For the TypeORM alternative when a large existing codebase uses decorator-based entity classes — TypeORM’s ActiveRecord and DataMapper patterns are more familiar to Django/Rails developers but have weaker TypeScript types than Prisma’s generated client, see the ORM comparison guide. The Claude Skills 360 bundle includes Prisma Extensions skill sets covering soft delete, multi-tenancy, and computed fields. Start with the free tier to try database extension generation.