Skip to main content

Audit Trail

Rhino includes an automatic audit trail system that records every change to your models. When hasAuditTrail: true is set on a model registration, create, update, delete, restore, and force-delete operations are logged with full before/after snapshots, user context, and request metadata.

Enabling the Audit Trail​

Set hasAuditTrail: true on the model registration in src/rhino.config.ts. Use auditExclude to keep sensitive fields out of the snapshots:

src/rhino.config.ts
posts: {
model: 'post',
hasAuditTrail: true,
// Exclude sensitive fields from audit logs
auditExclude: ['password', 'rememberToken'],
},

Tracked Events​

Rhino's audit service records changes around each CRUD operation automatically:

OperationAction LoggedOld ValuesNew Values
CreatecreatednullAll attributes
UpdateupdatedChanged fields (original values)Changed fields (new values)
Soft deletedeletedAll attributesnull
Force deleteforce_deletedAll attributesnull
RestorerestorednullAll attributes

For update events, only the dirty (changed) fields are recorded, not the entire record. If no fields actually changed, no audit log entry is created.

AuditLog Model​

Audit entries are stored in the audit_logs table. Define the AuditLog model in your Prisma schema:

prisma/schema.prisma
model AuditLog {
id Int @id @default(autoincrement())
auditableType String
auditableId String
action String
oldValues String?
newValues String?
userId Int?
organizationId Int?
ipAddress String?
userAgent String?
createdAt DateTime @default(now())

@@index([auditableType, auditableId])
@@map("audit_logs")
}

Each entry contains:

ColumnTypeDescription
idIntPrimary key
auditableTypeStringThe model table name (e.g., 'posts', 'users')
auditableIdStringThe primary key of the audited record
actionStringOne of: created, updated, deleted, restored, force_deleted
oldValuesString?JSON snapshot of values before the change
newValuesString?JSON snapshot of values after the change
userIdInt?The ID of the user who performed the action
organizationIdInt?The organization context (if multi-tenancy enabled)
ipAddressString?The IP address of the request
userAgentString?The User-Agent header of the request
createdAtDateTimeWhen the log entry was created

The auditableType and auditableId columns form a polymorphic reference to the audited record. oldValues / newValues are stored as JSON strings.

Excluding Fields​

By default, the password and rememberToken fields are excluded from audit log snapshots. Set auditExclude on the registration to customize which fields are excluded:

src/rhino.config.ts
users: {
model: 'user',
hasAuditTrail: true,
auditExclude: ['password', 'rememberToken', 'apiToken', 'stripeSecret'],
},

Excluded fields will never appear in oldValues or newValues, preventing sensitive data from being stored in the audit log.

Request Context​

The audit trail automatically captures the current HTTP request context from the NestJS request:

  • userId -- from the authenticated user (req.user.id)
  • organizationId -- from req.organization.id (if multi-tenancy is enabled)
  • ipAddress -- from the request IP
  • userAgent -- from the User-Agent header

When running outside an HTTP request (e.g., scripts, tests, queue workers), these fields are set to null. Audit logging never throws an error when context is unavailable -- it silently records what it can.

Querying Audit Logs​

Because AuditLog is a plain Prisma model, you query it directly via the Prisma client:

// All changes by a specific user
const userChanges = await prisma.auditLog.findMany({
where: { userId },
orderBy: { createdAt: 'desc' },
});

// All deletions across all models
const deletions = await prisma.auditLog.findMany({
where: { action: 'deleted' },
orderBy: { createdAt: 'desc' },
});

// All entries for a specific record
const postHistory = await prisma.auditLog.findMany({
where: { auditableType: 'posts', auditableId: '1' },
orderBy: { createdAt: 'desc' },
});

for (const log of postHistory) {
console.log(log.action); // 'created', 'updated', etc.
console.log(JSON.parse(log.oldValues ?? 'null')); // Previous state
console.log(JSON.parse(log.newValues ?? 'null')); // New state
console.log(log.userId); // Who made the change
console.log(log.createdAt); // When it happened
}

Migration​

Add the AuditLog model to prisma/schema.prisma (see above) and run a migration to create the audit_logs table:

terminal
npx prisma migrate dev

Fail-Safe Design​

Audit logging is designed to never break the main operation. All audit log creation is wrapped in try/catch blocks. If the AuditLog model is not available, the table does not exist, or a database error occurs during logging, the primary CRUD operation completes successfully and the audit failure is silently ignored.