Audit log extension for Prisma — records before/after state of every mutation with actor metadata, reason, and custom fields. Driver-agnostic: store logs in SQLite, PostgreSQL, or plain NDJSON files.
bun add @stratapkg/chroniclePick a driver:
| Import | Requires |
|---|---|
@stratapkg/chronicle/sqlite |
bun:sqlite or better-sqlite3 |
@stratapkg/chronicle/postgres |
any Prisma adapter (logs to same DB) |
@stratapkg/chronicle/file |
nothing (NDJSON files) |
import { PrismaClient } from './generated/client'
import { withChronicle } from '@stratapkg/chronicle'
import { SqliteDriver } from '@stratapkg/chronicle/sqlite'
import { Database } from 'bun:sqlite'
const auditDb = new Database('audit.db')
const prisma = new PrismaClient({ adapter })
.$extends(withChronicle({
driver: new SqliteDriver(auditDb),
models: ['User', 'Order', 'Payment'],
}))await prisma.user.update({
where: { id: 1 },
data: { role: 'admin' },
})Audit record:
{
"model": "User",
"recordId": "1",
"action": "UPDATE",
"before": { "role": "user", "name": "Alice" },
"after": { "role": "admin", "name": "Alice" },
"actor": null,
"timestamp": "2025-06-01T12:00:00Z"
}await prisma.user.update({
where: { id: 1 },
data: { role: 'admin' },
chronicle: {
actor: currentUserId,
reason: 'promoted via request #123',
meta: { ip: '192.168.1.1', userAgent: req.headers['user-agent'] },
},
})The chronicle field is typed and available on all mutating operations — create, update, upsert, delete, createMany, updateMany, deleteMany.
// all changes to a record
const history = await prisma.$chronicle.findMany({
model: 'User',
recordId: '1',
})
// all changes by actor
const byActor = await prisma.$chronicle.findMany({
actor: currentUserId,
from: new Date('2025-01-01'),
})| Operation | before | after |
|---|---|---|
create |
null |
new record |
update |
old record | new record |
upsert |
old record or null |
new record |
delete |
old record | null |
updateMany |
— | summary with count |
deleteMany |
— | summary with count |
before snapshot is fetched automatically before the mutation via findUnique. Can be disabled per-model for performance:
withChronicle({
driver,
models: {
User: { trackBefore: true },
Session: { trackBefore: false }, // high-frequency, skip before snapshot
},
})interface ChronicleDriver {
log(entry: AuditEntry): Promise<void>
query(filter: AuditFilter): Promise<AuditEntry[]>
}- Prisma >= 7.0
- Any Prisma adapter
MIT