Skip to main content

Versioning & Audit Logs

Radish CLI generates comprehensive versioning and audit trail capabilities for tracking entity changes over time. This supports compliance requirements, debugging, rollback scenarios, and approval workflows.

Overview​

Radish provides three levels of change tracking:

  1. Full Versioning (versioning: full) - Complete entity snapshots with revert capability
  2. Simple Versioning (versioning: simple) - Lightweight audit logs with field diffs
  3. Field-Level Tracking (trackChanges: true) - Enhanced metadata for critical fields with approval workflows

These can be used independently or combined for maximum flexibility.

Quick Comparison​

FeatureSimple VersioningFull Versioning
StorageField diffs only (~100-500 bytes)Complete snapshots (~1-10KB)
Rollback❌ No✅ Yes - POST /revert/{version}
Comparison❌ No✅ Yes - GET /versions/compare
Audit Trail✅ Yes - /audit endpoint✅ Yes - /versions endpoint
Use CaseActivity tracking, debuggingCompliance, rollback workflows
Collections{entity}AuditLog{entity}Version

Full Versioning​

Full versioning stores complete snapshots of entities at each change, enabling you to:

  • View exact state at any point in history
  • Revert to previous versions
  • Compare versions side-by-side

When to Use​

  • ✅ Legal/compliance requirements
  • ✅ Need to revert changes (e-commerce pricing, contracts)
  • ✅ Document versioning
  • ✅ Financial records (regulatory requirements)
  • ❌ High-volume updates (consider simple versioning instead)

Configuration​

{
"entities": {
"Product": {
"versioning": "full",
"plural": "products",
"fields": {
"name": { "type": "string", "required": true },
"price": { "type": "float", "required": true },
"description": { "type": "string" }
}
}
}
}

Generated Artifacts​

  • Contracts: productVersionSchema, ProductVersion type in .radish/lib/datalayer/contracts/product-version.ts
  • Repository: ProductVersionRepo in .radish/lib/datalayer/core/repos/product-version.repo.ts
  • Model: ProductVersion Mongoose model in .radish/lib/datalayer/core/models/product-version.model.ts
  • Routes: Version history and revert endpoints

Storage Cost​

Each change stores a complete entity snapshot (~1-10KB per version typically).

Simple Versioning​

Simple versioning stores lightweight audit logs with field-level diffs, enabling you to:

  • Track what changed, when, and by whom
  • View change history
  • Debug issues

When to Use​

  • ✅ User activity tracking
  • ✅ Debugging and troubleshooting
  • ✅ Analytics and reporting
  • ✅ Storage-constrained environments
  • ✅ High-volume entities
  • ❌ Need to revert (use full versioning instead)

Configuration​

{
"entities": {
"Order": {
"versioning": "simple",
"plural": "orders",
"fields": {
"status": { "type": "enum", "values": ["pending", "shipped", "delivered"] },
"total": { "type": "float" }
}
}
}
}

Generated Artifacts​

  • Contracts: orderAuditLogSchema, OrderAuditLog type in .radish/lib/datalayer/contracts/order-auditlog.ts
  • Repository: OrderAuditLogRepo in .radish/lib/datalayer/core/repos/order-auditlog.repo.ts
  • Model: OrderAuditLog Mongoose model in .radish/lib/datalayer/core/models/order-auditlog.model.ts
  • Routes: Audit log access endpoints

Storage Cost​

Each change stores only field diffs (~100-500 bytes per change typically).

Field-Level Change Tracking​

Field-level tracking adds enhanced metadata and approval workflows for critical fields.

When to Use​

  • ✅ Price changes requiring approval
  • ✅ Sensitive data modifications
  • ✅ Compliance-critical fields
  • ✅ Fields needing change justification
  • ❌ All fields (use entity-level versioning instead)

Configuration​

{
"entities": {
"Invoice": {
"versioning": "full",
"plural": "invoices",
"fields": {
"customerId": { "type": "objectId", "ref": "Customer" },
"amount": {
"type": "float",
"required": true,
"trackChanges": true
},
"status": { "type": "enum", "values": ["draft", "sent", "paid"] }
}
}
}
}

Generated Artifacts​

  • Contracts: invoiceFieldChangeSchema, InvoiceFieldChange type in .radish/lib/datalayer/contracts/invoice-fieldchange.ts
  • Repository: InvoiceFieldChangeRepo with approval workflow methods
  • Model: InvoiceFieldChange Mongoose model
  • Routes: Field change history and approval endpoints

Features​

  • Tracks old value, new value, and metadata
  • Approval workflow (pending → approved/rejected)
  • Change reason documentation
  • Custom metadata support

Generated Contracts​

Full Versioning Schema​

import { productVersionSchema } from '@generated/datalayer/contracts/product-version';
import type { ProductVersion } from '@generated/datalayer/contracts/product-version';

// Schema definition
export const productVersionSchema = z.object({
id: z.string(),
entityId: z.string(),
version: z.number().int(),
snapshot: z.any(), // Complete entity state
changedBy: z.string(),
changeReason: z.string().optional(),
changeType: z.enum(['create', 'update', 'delete', 'archive', 'restore']),
changedFields: z.array(z.string()).optional(),
createdAt: z.string(),
updatedAt: z.string(),
}).strict();

export type ProductVersion = z.infer<typeof productVersionSchema>;

Simple Versioning Schema​

import { orderAuditLogSchema } from '@generated/datalayer/contracts/order-auditlog';
import type { OrderAuditLog } from '@generated/datalayer/contracts/order-auditlog';

// Schema definition
export const orderAuditLogSchema = z.object({
id: z.string(),
entityId: z.string(),
changedBy: z.string(),
changeType: z.enum(['create', 'update', 'delete', 'archive', 'restore']),
changedFields: z.array(z.string()).optional(),
changes: z.record(z.object({ // Field diffs
old: z.any(),
new: z.any(),
})).optional(),
changeReason: z.string().optional(),
createdAt: z.string(),
updatedAt: z.string(),
}).strict();

export type OrderAuditLog = z.infer<typeof orderAuditLogSchema>;

Field Change Schema​

import { invoiceFieldChangeSchema } from '@generated/datalayer/contracts/invoice-fieldchange';
import type { InvoiceFieldChange } from '@generated/datalayer/contracts/invoice-fieldchange';

// Schema definition
export const invoiceFieldChangeSchema = z.object({
id: z.string(),
entityId: z.string(),
fieldName: z.string(),
oldValue: z.any(),
newValue: z.any(),
changedBy: z.string(),
changeReason: z.string().optional(),
approvedBy: z.string().optional(),
approvalStatus: z.enum(['pending', 'approved', 'rejected']).optional(),
metadata: z.record(z.any()).optional(),
createdAt: z.string(),
updatedAt: z.string(),
}).strict();

export type InvoiceFieldChange = z.infer<typeof invoiceFieldChangeSchema>;

API Endpoints​

Full Versioning Endpoints​

Get Version History​

GET /api/v1/products/{id}/versions?limit=20&cursor=...
Authorization: Bearer <token>

Response:

{
"items": [
{
"id": "...",
"entityId": "product-123",
"version": 5,
"snapshot": {
"id": "product-123",
"name": "MacBook Pro",
"price": 2499,
"description": "Latest model"
},
"changedBy": "user-456",
"changeType": "update",
"changedFields": ["price"],
"createdAt": "2026-03-10T14:30:00Z",
"updatedAt": "2026-03-10T14:30:00Z"
}
],
"total": 5,
"hasMore": false
}

Get Specific Version​

GET /api/v1/products/{id}/versions/{version}
Authorization: Bearer <token>

Response:

{
"id": "...",
"entityId": "product-123",
"version": 3,
"snapshot": {
"id": "product-123",
"name": "MacBook Pro",
"price": 2299
},
"changedBy": "user-789",
"createdAt": "2026-03-09T10:15:00Z"
}

Compare Versions​

GET /api/v1/products/{id}/versions/compare?a=3&b=5
Authorization: Bearer <token>

Response:

{
"entityId": "product-123",
"versionA": {
"version": 3,
"createdAt": "2026-03-09T10:15:00Z",
"changedBy": "user-789"
},
"versionB": {
"version": 5,
"createdAt": "2026-03-10T14:30:00Z",
"changedBy": "user-456"
},
"changes": [
{
"field": "price",
"oldValue": 2299,
"newValue": 2499,
"changed": true
},
{
"field": "status",
"oldValue": "draft",
"newValue": "active",
"changed": true
}
],
"totalChanges": 2
}

Revert to Version​

POST /api/v1/products/{id}/revert/{version}
Authorization: Bearer <token>
Content-Type: application/json

{
"changeReason": "Reverting accidental price change"
}

Response:

{
"success": true,
"revertedTo": 3,
"newVersion": 6,
"entity": {
"id": "product-123",
"name": "MacBook Pro",
"price": 2299
}
}

Simple Versioning Endpoints​

Get Audit Log​

GET /api/v1/orders/{id}/audit?limit=20&cursor=...
Authorization: Bearer <token>

Response:

{
"items": [
{
"id": "...",
"entityId": "order-123",
"changedBy": "user-456",
"changeType": "update",
"changedFields": ["status", "shippedAt"],
"changes": {
"status": {
"old": "pending",
"new": "shipped"
},
"shippedAt": {
"old": null,
"new": "2026-03-10T14:30:00Z"
}
},
"changeReason": "Order shipped via FedEx",
"createdAt": "2026-03-10T14:30:00Z"
}
],
"total": 12,
"hasMore": false
}

Field Change Endpoints​

Get Field Changes​

GET /api/v1/invoices/{id}/field-changes?fieldName=amount
Authorization: Bearer <token>

Response:

{
"items": [
{
"id": "...",
"entityId": "invoice-123",
"fieldName": "amount",
"oldValue": 1000,
"newValue": 1200,
"changedBy": "user-456",
"changeReason": "Added consulting hours",
"approvalStatus": "pending",
"createdAt": "2026-03-10T14:30:00Z"
}
]
}

Get Pending Approvals​

GET /api/v1/invoices/{id}/field-changes?approvalStatus=pending
Authorization: Bearer <token>

Approve/Reject Change​

PUT /api/v1/invoices/{id}/field-changes/{changeId}
Authorization: Bearer <admin-token>
Content-Type: application/json

{
"approvalStatus": "approved",
"approvedBy": "admin-123"
}

Use Cases​

E-Commerce Price Management​

{
"Product": {
"versioning": "full",
"fields": {
"name": { "type": "string", "required": true },
"price": {
"type": "float",
"required": true,
"trackChanges": true
},
"cost": { "type": "float" }
}
}
}

Workflow:

  1. User updates price → Full snapshot saved automatically
  2. Field change record created with old/new price
  3. Admin approves/rejects price change
  4. Can revert to previous version if needed

Compliance & Audit​

{
"Contract": {
"versioning": "full",
"fields": {
"terms": { "type": "string" },
"effectiveDate": { "type": "isoDate" },
"signedBy": { "type": "objectId", "ref": "User" }
}
}
}

Benefits:

  • Complete audit trail for regulators
  • Prove when changes were made and by whom
  • Revert unauthorized modifications
  • Compare contract versions side-by-side

User Activity Tracking​

{
"UserProfile": {
"versioning": "simple",
"fields": {
"displayName": { "type": "string" },
"avatar": { "type": "url" },
"bio": { "type": "string" }
}
}
}

Benefits:

  • Low storage overhead
  • Track user modifications
  • Debug profile issues
  • Analytics on profile updates

Mixed Strategy​

{
"Order": {
"versioning": "simple",
"fields": {
"items": { "type": "array" },
"total": {
"type": "float",
"trackChanges": true
},
"status": { "type": "enum", "values": ["pending", "confirmed", "shipped"] }
}
}
}

Combines:

  • Simple audit log for all changes
  • Enhanced tracking for critical total field
  • Approval workflow for financial data

Service Integration​

Services automatically create version/audit records when entities change.

Full Versioning Example​

import { ProductService } from '@generated/datalayer/services';
import { ProductVersionRepo } from '@core/repos/product-version.repo';
import type { AuthContext } from '@generated/datalayer/core/auth';

const ctx: AuthContext = {
userId: 'user-123',
roles: ['USER'],
permissions: ['product:edit:own']
};

// Create product (version 1 created automatically)
const product = await ProductService.create(ctx, {
name: 'MacBook Pro',
price: 2499,
description: 'Latest model'
});

// Update product (version 2 created automatically)
await ProductService.update(ctx, product.id, {
price: 2299
});

// Get version history
const versions = await ProductVersionRepo.listVersions(product.id);
console.log(versions.length); // 2

// Get specific version
const v1 = await ProductVersionRepo.getVersion(product.id, 1);
console.log(v1.snapshot.price); // 2499

// Revert to version 1 (creates version 3 with v1 data)
await ProductService.revert(ctx, product.id, 1);

Simple Versioning Example​

import { OrderService } from '@generated/datalayer/services';
import { OrderAuditLogRepo } from '@core/repos/order-auditlog.repo';

// Update order (audit log created automatically)
await OrderService.update(ctx, orderId, {
status: 'shipped',
shippedAt: new Date().toISOString()
});

// Get audit log
const auditLog = await OrderAuditLogRepo.getAuditLog(orderId);
console.log(auditLog.items[0].changes);
// { status: { old: 'pending', new: 'shipped' }, ... }

Best Practices​

1. Choose the Right Strategy​

Full Versioning:

  • ✅ Legal/compliance requirements
  • ✅ Need to revert changes
  • ✅ Moderate change frequency
  • ❌ High-volume updates (use simple instead)

Simple Versioning:

  • ✅ High-volume entities
  • ✅ Storage-constrained environments
  • ✅ Debugging and analytics
  • ❌ Need to revert (use full instead)

Field-Level Tracking:

  • ✅ Critical fields only
  • ✅ Approval workflows needed
  • ✅ Sensitive data
  • ❌ All fields (use entity-level instead)

2. Combine Strategies​

{
"Invoice": {
"versioning": "full",
"fields": {
"amount": {
"type": "float",
"trackChanges": true
},
"items": { "type": "array" }
}
}
}

This provides:

  • Full entity history for compliance
  • Enhanced tracking for sensitive amount field
  • Approval workflow for financial data

3. Storage Considerations​

Full Versioning:

  • Storage grows with each change
  • Monitor collection sizes
  • Consider retention policies (e.g., keep 90 days)
  • Archive old versions to cold storage

Simple Versioning:

  • Much lower storage footprint (~10-20x smaller)
  • Suitable for high-volume entities
  • Still provides valuable audit trail

4. Performance Tips​

  • Index entityId in version/audit collections (done automatically)
  • Use cursor pagination for version history queries
  • Consider caching latest version
  • Archive old versions periodically

5. Security & Permissions​

Versioning respects entity permissions:

  • Users must have {entity}:view permission to see history
  • Only authorized users can revert changes
  • Field change approvals require appropriate permissions
  • Change records include changedBy user ID

6. Testing​

import { ProductService } from '@generated/datalayer/services';
import { ProductVersionRepo } from '@core/repos/product-version.repo';

// Test versioning is working
const product = await ProductService.create(ctx, {
name: 'Widget',
price: 99
});

await ProductService.update(ctx, product.id, { price: 149 });

const versions = await ProductVersionRepo.listVersions(product.id);
expect(versions.items).toHaveLength(2);
expect(versions.items[0].snapshot.price).toBe(149);
expect(versions.items[1].snapshot.price).toBe(99);

Migration Guide​

Adding Versioning to Existing Entities​

1. Add versioning to blueprint:

{
"Product": {
"versioning": "full",
"fields": {}
}
}

2. Regenerate code:

radish-cli create datalayer . --schema blueprints/app.types.json

3. Existing data:

  • Old records have no version history (expected)
  • Versioning starts tracking from next change
  • No migration required

Changing Versioning Strategy​

You can change from simple to full or vice versa:

{
"Product": {
"versioning": "full"
}
}

Note: Existing audit logs/versions remain unchanged. New changes use new strategy.

Troubleshooting​

Version History Not Appearing​

Check:

  1. Versioning enabled in schema: versioning: full or versioning: simple
  2. Code regenerated after schema change
  3. Service methods (create, update) being called (not direct repo access)
  4. No errors in service layer

Field Changes Not Tracked​

Check:

  1. Field has trackChanges: true in schema
  2. Updates going through service layer
  3. Field actually changing (old !== new)

Revert Not Working​

Check:

  1. Using versioning: full (revert requires full snapshots)
  2. User has appropriate permissions
  3. Version exists and is accessible
  4. No validation errors in snapshot data

Complete Example​

{
"entities": {
"Product": {
"plural": "products",
"versioning": "full",
"fields": {
"name": { "type": "string", "required": true },
"price": {
"type": "float",
"required": true,
"trackChanges": true
},
"description": { "type": "string" },
"category": { "type": "enum", "values": ["electronics", "clothing", "books"] }
}
},
"UserActivity": {
"plural": "user-activities",
"versioning": "simple",
"fields": {
"action": { "type": "string" },
"details": { "type": "object" },
"ipAddress": { "type": "string" }
}
}
}
}

Generated files:

.radish/lib/datalayer/
├── contracts/
│ ├── product-version.ts
│ ├── product-fieldchange.ts
│ └── useractivity-auditlog.ts
├── core/
│ ├── models/
│ │ ├── product-version.model.ts
│ │ ├── product-fieldchange.model.ts
│ │ └── useractivity-auditlog.model.ts
│ └── repos/
│ ├── product-version.repo.ts
│ ├── product-fieldchange.repo.ts
│ └── useractivity-auditlog.repo.ts

Usage:

import { ProductService } from '@generated/datalayer/services';
import type { AuthContext } from '@generated/datalayer/core/auth';

const ctx: AuthContext = { userId: 'user-123', roles: ['USER'], permissions: [] };

// Product with full versioning + field tracking
const product = await ProductService.create(ctx, {
name: 'MacBook Pro',
price: 2499,
description: 'Latest model',
category: 'electronics'
});

// Update creates version + field change
await ProductService.update(ctx, product.id, { price: 2299 });

// View history via API
const versions = await fetch(`/api/v1/products/${product.id}/versions`);
const priceChanges = await fetch(`/api/v1/products/${product.id}/field-changes?fieldName=price`);

// Revert via API
await fetch(`/api/v1/products/${product.id}/revert/1`, { method: 'POST' });

// User activity with simple versioning
import { UserActivityService } from '@generated/datalayer/services';

await UserActivityService.create(ctx, {
action: 'login',
details: { method: 'oauth' },
ipAddress: '192.168.1.1'
});

// View audit log via API
const auditLog = await fetch(`/api/v1/user-activities/${activityId}/audit`);

Summary​

  • Full Versioning (versioning: full) - Complete snapshots + revert capability
  • Simple Versioning (versioning: simple) - Lightweight audit logs
  • Field Tracking (trackChanges: true) - Enhanced metadata + approval workflow
  • Combine strategies for maximum flexibility
  • Choose based on requirements, storage, and compliance needs
  • All versioning respects authentication and authorization

Next Steps​