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/server/repos/product-version.repo.ts
  • Model: ProductVersion Mongoose model in .radish/lib/datalayer/server/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/server/repos/order-auditlog.repo.ts
  • Model: OrderAuditLog Mongoose model in .radish/lib/datalayer/server/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 # Entity-level versioning (optional)
plural: invoices
fields:
customerId: { type: objectId, ref: Customer }
amount:
type: float
required: true
trackChanges: true # Enhanced field tracking
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 # Need to revert price changes
fields:
name: { type: string, required: true }
price:
type: float
required: true
trackChanges: true # Price changes need approval
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 # Legal requirement for complete history
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 # Just need to know what changed
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 # Track all changes
fields:
items: { type: array }
total:
type: float
trackChanges: true # Total changes need approval
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 '@generated/datalayer/server/repos/ (internal - use services instead)product-version.repo';
import type { AuthContext } from '@generated/server/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 '@generated/datalayer/server/repos/ (internal - use services instead)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 # Can revert entire invoice
fields:
amount:
type: float
trackChanges: true # Amount changes need approval
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 '@generated/datalayer/server/repos/ (internal - use services instead)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 # or simple
# ... existing fields

2. Regenerate code:

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

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:

# Before
Product:
versioning: simple

# After
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

# blueprints/app.types.yml
entities:
Product:
plural: products
versioning: full # Complete history with revert
fields:
name: { type: string, required: true }
price:
type: float
required: true
trackChanges: true # Price needs approval
description: { type: string }
category: { type: enum, values: [electronics, clothing, books] }

UserActivity:
plural: user-activities
versioning: simple # Lightweight audit only
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
├── server/
│ ├── 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/server/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