Picture this: A critical database record is modified or deleted in production. Customer support is flooded, and your manager asks, "Who did this, what was changed, and what did it look like before?"
If you don't have audit logging set up, you're stuck digging through database transaction logs, parsing raw server outputs, or doing forensic database recovery. It's stressful, slow, and completely avoidable.
But building audit logging is notoriously tedious:
- Database Triggers are hard to maintain, slow, and don't know anything about your application context (like the HTTP Request IP or the active User Session).
- Manual Service Logging requires you to write boilerplate code around every database update, mutation, and delete in your entire codebase. One missing line, and your audit log is incomplete.
To solve this once and for all, I built Prisma Audit Log.
Itβs a native Prisma Client extension that automatically intercepts your queries (create, update, delete, upsert, and batch operations) to log changes. Because it runs directly inside your application code, it captures full request context and user details seamlessly.
Here is how to set it up and what it gives you.
1. The Audit Log Schema
First, add an AuditLog model to your schema.prisma file. This is where your audit records will be stored:
model AuditLog {
id String @id @default(cuid())
userId String? @map("user_id")
recordId String @map("record_id")
action String // "create" | "update" | "delete"
model String // E.g. "User", "Post"
oldData Json? @map("old_data")
newData Json? @map("new_data")
changedFields String[] @map("changed_fields")
ipAddress String? @map("ip_address")
userAgent String? @map("user_agent")
metadata Json?
createdAt DateTime @default(now()) @map("created_at")
@@map("audit_logs")
}
2. Basic Extension Setup
You can register the audit log extension on your existing PrismaClient in seconds:
import { PrismaClient } from "@prisma/client";
import { auditLogExtension } from "@explita/prisma-audit-log";
import { getAuthContext } from "@/lib/auth"; // Your auth/request storage
const prisma = new PrismaClient().$extends(
auditLogExtension({
// 1. Context-Aware Logging: Automatically capture user/network context
getContext: () => {
const session = getAuthContext();
return {
userId: session?.userId || "anonymous",
ipAddress: session?.ip,
userAgent: session?.userAgent,
metadata: {
tenantId: session?.tenantId,
},
};
},
// 2. Data Sanitization: Mask passwords or security tokens globally
maskFields: ["password", "token", "creditCard"],
maskValue: "[REDACTED]",
// 3. Fine-grained filtering per model
fieldFilters: {
User: {
exclude: ["refreshToken", "passwordSalt"],
},
Payment: {
include: ["id", "amount", "status", "createdAt"], // Whitelist only
},
},
// 4. Skip unnecessary logging (e.g. high-frequency sessions)
skip: ({ model, operation }) => {
if (model === "Session" && operation === "create") return true;
return false;
},
})
);
3. What Happens Under the Hood?
Once registered, you write your normal database queries, and Prisma Audit Log handles the rest behind the scenes.
For example, when you perform a normal Prisma update:
await prisma.user.update({
where: { id: "user_abc" },
data: {
email: "newemail@example.com",
role: "ADMIN",
},
});
The extension intercepts this, calculates the diff between the database state before and after, and creates a record that looks like this:
{
"id": "log_12345",
"userId": "admin_user_id",
"recordId": "user_abc",
"action": "update",
"model": "User",
"oldData": {
"id": "user_abc",
"email": "oldemail@example.com",
"role": "USER"
},
"newData": {
"id": "user_abc",
"email": "newemail@example.com",
"role": "ADMIN"
},
"changedFields": ["email", "role"],
"ipAddress": "192.168.1.1",
"userAgent": "Mozilla/5.0...",
"metadata": { "tenantId": "tenant_1" },
"createdAt": "2026-05-21T13:15:34Z"
}
4. Key Developer-First Features
Here are some details I built in to keep database logs clean, performant, and safe:
π Noise-Free Update Logs
Have you ever seen audit logs filled with thousands of rows where nothing actually changed? Prisma updates often touch a model's updatedAt field or re-save unmodified fields. Prisma Audit Log automatically detects if only the timestamp fields changed, and quietly skips creating an audit entry. Your logs stay clean and noise-free.
β‘ Efficient Batch Processing
When you run bulk operations like createMany, updateMany, or deleteMany, inserting audit logs one-by-one is a performance killer. The extension groups and batches these operations, falling back gracefully to single inserts only when necessary.
π Send Logs Anywhere
By default, the extension saves audit logs directly to your AuditLog table using the Prisma schema above. But if you want to stream logs to an external service (like Datadog, Axiom, or a Kafka queue), you can override the target using a custom logger callback:
auditLogExtension({
logger: async (logs) => {
// Send logs to Elasticsearch, Datadog, or cloud storage
await datadogClient.send(logs);
}
})
Give it a try! π
Prisma Audit Log is open-source (MIT licensed) and ready for use. If you're building systems that require database compliance, transparency, or debugging reassurance, give it a spin:
-
NPM:
npm i @explita/prisma-audit-log - GitHub: github.com/explita/prisma-audit-log
If this helps you sleep a bit better at night knowing your database history is safe, I'd love a GitHub β! Let me know in the comments if you have any questions or feature suggestions.
Top comments (0)