31 KiB
Template Version History
Overview
The Template Version History system provides comprehensive audit trails for email template changes with automatic version creation, rollback capability, and change tracking. Every template save creates a new version snapshot, preserving the complete history of modifications with metadata about who changed what and why.
Key Features:
- Automatic Version Creation — Every save creates a new version (no manual versioning)
- Auto-Incrementing Version Numbers — Sequential numbering (1, 2, 3...) per template
- Complete Snapshots — Stores subject line, HTML content, and text content
- Change Notes — Optional admin-provided descriptions of changes
- User Attribution — Tracks who created each version
- Rollback Capability — Restore any previous version (non-destructive)
- Version Comparison — Visual diff between any two versions
- Audit Trail — Full history for compliance and debugging
Benefits:
- Accident Recovery — Undo mistakes by rolling back to previous version
- Change Tracking — See what changed and when
- Compliance — Audit trail for regulatory requirements
- Collaboration — Multiple admins can see each other's changes
- Experimentation — Safely test changes knowing you can rollback
- Documentation — Change notes explain why changes were made
Architecture
flowchart TB
subgraph "Version Creation Flow"
Save[Admin Saves Template]
FindMax[Find Max Version Number]
Increment[Increment to Next Version]
CreateVersion[Create EmailTemplateVersion]
UpdateTemplate[Update EmailTemplate]
Save --> FindMax
FindMax --> Increment
Increment --> CreateVersion
CreateVersion --> UpdateTemplate
end
subgraph "Database Models"
Template[(EmailTemplate)]
Versions[(EmailTemplateVersion)]
Template -->|1:N| Versions
end
subgraph "Version Data"
Snapshot[Content Snapshot<br/>subject, HTML, text]
Meta[Metadata<br/>version number, change notes]
Attribution[Attribution<br/>created by user, timestamp]
Snapshot --> Versions
Meta --> Versions
Attribution --> Versions
end
subgraph "Version Operations"
List[List Version History]
Compare[Compare Two Versions]
Rollback[Rollback to Version]
View[View Version Details]
Versions --> List
Versions --> Compare
Versions --> Rollback
Versions --> View
end
subgraph "Rollback Flow"
SelectVersion[Select Old Version]
LoadContent[Load Old Content]
UpdateCurrent[Update Current Template]
CreateNewVersion[Create New Version<br/>'Rolled back to vX']
SelectVersion --> LoadContent
LoadContent --> UpdateCurrent
UpdateCurrent --> CreateNewVersion
CreateNewVersion --> Versions
end
Save --> Template
CreateVersion --> Versions
Rollback --> Template
style Save fill:#4a90e2,color:#fff
style CreateVersion fill:#50c878,color:#fff
style Rollback fill:#ff6b6b,color:#fff
Component Responsibilities:
- EmailTemplateVersion — Version snapshot storage with metadata
- Version Service — Auto-increment logic, version creation
- Rollback Service — Restore old version as new version (non-destructive)
- Comparison Service — Diff generation between versions
- Audit Log — User attribution and change notes
Database Model
EmailTemplateVersion Schema
Table: email_template_versions
| Field | Type | Description |
|---|---|---|
id |
String (CUID) | Primary key |
templateId |
String | Foreign key to EmailTemplate |
versionNumber |
Int | Auto-incremented version (1, 2, 3...) |
subjectLine |
String | Subject line snapshot |
htmlContent |
Text | HTML content snapshot |
textContent |
Text | Plain text content snapshot |
changeNotes |
String (optional) | Admin-provided change description |
createdByUserId |
String (optional) | User who created this version |
createdAt |
DateTime | Version creation timestamp |
Relations:
template— EmailTemplate (N:1)createdBy— User (N:1)
Constraints:
- Unique index on
(templateId, versionNumber)for version lookup - Auto-increment logic in service layer (finds max + 1)
- No ON DELETE CASCADE (preserve versions even if template deleted)
Prisma Schema:
model EmailTemplateVersion {
id String @id @default(cuid())
templateId String
versionNumber Int
subjectLine String
htmlContent String @db.Text
textContent String @db.Text
changeNotes String?
createdByUserId String?
createdAt DateTime @default(now())
template EmailTemplate @relation(fields: [templateId], references: [id])
createdBy User? @relation(fields: [createdByUserId], references: [id])
@@unique([templateId, versionNumber])
@@index([templateId])
@@index([createdAt])
@@map("email_template_versions")
}
Version Creation
Automatic Versioning on Save
When Versions Are Created:
- Admin saves template via EmailTemplateEditorPage
- API
PUT /api/email-templates/:idendpoint called - Version created BEFORE updating template (snapshot current state)
Auto-Increment Logic:
// api/src/modules/email-templates/email-templates.service.ts
async function createVersion(
templateId: string,
options: {
changeNotes?: string;
createdByUserId?: string;
}
) {
// 1. Find max version number for this template
const maxVersion = await prisma.emailTemplateVersion.findFirst({
where: { templateId },
orderBy: { versionNumber: 'desc' },
select: { versionNumber: true },
});
const nextVersion = (maxVersion?.versionNumber || 0) + 1;
// 2. Load current template content
const template = await prisma.emailTemplate.findUnique({
where: { id: templateId },
});
if (!template) {
throw new Error('Template not found');
}
// 3. Create version snapshot
const version = await prisma.emailTemplateVersion.create({
data: {
templateId,
versionNumber: nextVersion,
subjectLine: template.subjectLine,
htmlContent: template.htmlContent,
textContent: template.textContent,
changeNotes: options.changeNotes,
createdByUserId: options.createdByUserId,
},
});
return version;
}
Save Template with Versioning:
// api/src/modules/email-templates/email-templates.routes.ts
router.put('/:id', requireRole(SUPER_ADMIN), async (req, res) => {
const { id } = req.params;
const { subjectLine, htmlContent, textContent, changeNotes } = req.body;
try {
// 1. Create version BEFORE updating (snapshot current state)
await createVersion(id, {
changeNotes,
createdByUserId: req.user!.id,
});
// 2. Update template with new content
const updatedTemplate = await prisma.emailTemplate.update({
where: { id },
data: {
subjectLine,
htmlContent,
textContent,
updatedByUserId: req.user!.id,
},
});
res.json(updatedTemplate);
} catch (error) {
logger.error('Failed to save template', { error, templateId: id });
res.status(500).json({ error: 'Failed to save template' });
}
});
Important: Version is created BEFORE updating template, so version snapshots the OLD content (not the new content). This preserves the exact state before the change.
Version Number Sequence
Sequence Rules:
- Starts at 1 for first version
- Increments by 1 for each save
- Per-template sequence (not global)
- No gaps in sequence
Example Timeline:
| Action | Version | Subject | HTML | Change Notes |
|---|---|---|---|---|
| Create template | 1 | "Welcome!" | <p>Hello</p> |
(initial version) |
| Edit subject | 2 | "Welcome to Our Platform!" | <p>Hello</p> |
"Made subject more descriptive" |
| Add content | 3 | "Welcome to Our Platform!" | <p>Hello {{USER_NAME}}</p> |
"Added user name variable" |
| Rollback to v1 | 4 | "Welcome!" | <p>Hello</p> |
"Rolled back to version 1" |
Note: Rollback creates NEW version (v4 in example), doesn't delete v2 and v3. This preserves complete audit trail.
Change Notes
Purpose: Describe what changed and why (audit trail documentation)
When Prompted:
- EmailTemplateEditorPage shows "Change Notes" field on save
- Optional but recommended
- Stored in
changeNotesfield
Examples:
Good Change Notes:
- "Added phone number conditional block"
- "Fixed typo in subject line"
- "Updated shift location variable to include address"
- "Removed deprecated campaign URL variable"
- "Rolled back to version 5 due to rendering issue"
Poor Change Notes:
- "update" (not descriptive)
- "changes" (too vague)
- "" (empty, no context)
Implementation:
// EmailTemplateEditorPage.tsx
const [saveModalVisible, setSaveModalVisible] = useState(false);
const [changeNotes, setChangeNotes] = useState('');
const handleSave = async () => {
await api.put(`/api/email-templates/${id}`, {
subjectLine,
htmlContent,
textContent,
changeNotes: changeNotes || undefined, // Optional
});
message.success('Template saved successfully');
navigate('/app/email-templates');
};
// Modal UI
<Modal title="Save Template" visible={saveModalVisible} onOk={handleSave}>
<Form.Item label="Change Notes (optional)">
<TextArea
value={changeNotes}
onChange={(e) => setChangeNotes(e.target.value)}
placeholder="Describe what changed in this version"
rows={4}
/>
</Form.Item>
</Modal>
Admin Workflow
Viewing Version History
Step 1: Open Template Detail
- EmailTemplatesPage → click template row
- Opens template detail modal
Step 2: Navigate to "Version History" Tab
- Click "Version History" tab
- Shows table of all versions
Version History Table Columns:
- Version — Version number (e.g., "v3")
- Created — Timestamp (e.g., "2026-03-15 14:23")
- Created By — User name (e.g., "John Doe")
- Change Notes — Description of changes
- Actions — View, Compare, Restore buttons
Sorting:
- Default: Descending by version number (newest first)
- Can sort by created date or version number
Viewing Version Details
Step 1: Click "View" Button
- Version history table → click version row → "View" button
Step 2: Version Detail Modal
- Shows version metadata:
- Version number
- Created by user
- Created timestamp
- Change notes
- Shows content snapshot:
- Subject line
- HTML content (scrollable textarea)
- Text content (scrollable textarea)
Step 3: Preview Rendered Version
- Click "Preview" button
- Renders HTML with sample data
- Shows how email looked at that version
Comparing Versions
Step 1: Select Two Versions
- Version history table → checkbox on two version rows
- Click "Compare Selected" button
Step 2: Comparison Modal
- Side-by-side diff view:
- Left: Older version
- Right: Newer version
- Highlighting:
- Green: Added lines
- Red: Deleted lines
- Yellow: Modified lines
Comparison Sections:
- Subject Line Diff — Shows changes in subject
- HTML Content Diff — Line-by-line HTML diff
- Text Content Diff — Line-by-line text diff
Implementation:
import { diffLines } from 'diff';
function renderDiff(oldContent: string, newContent: string) {
const diff = diffLines(oldContent, newContent);
return diff.map((part, index) => {
let color = 'black';
let backgroundColor = 'transparent';
if (part.added) {
color = 'green';
backgroundColor = '#e6ffed';
} else if (part.removed) {
color = 'red';
backgroundColor = '#ffebe9';
}
return (
<pre
key={index}
style={{
color,
backgroundColor,
margin: 0,
padding: '2px 4px',
fontFamily: 'monospace',
fontSize: 12,
}}
>
{part.value}
</pre>
);
});
}
Rolling Back to Previous Version
Step 1: Select Version to Restore
- Version history table → click version row
Step 2: Click "Restore" Button
- Opens confirmation modal
Step 3: Confirm Rollback
- Modal shows:
- Version being restored (e.g., "Version 5")
- Warning: "This will create a new version with this content"
- Change notes field (pre-filled: "Rolled back to version 5")
Step 4: Confirm and Save
- Click "Confirm Restore"
- Creates new version (e.g., v10) with content from v5
- Current template updated to v5 content
- Redirects to EmailTemplatesPage
Rollback Process:
// api/src/modules/email-templates/email-templates.routes.ts
router.post('/:id/rollback/:versionNumber', requireRole(SUPER_ADMIN), async (req, res) => {
const { id } = req.params;
const versionNumber = parseInt(req.params.versionNumber);
try {
// 1. Load version to restore
const versionToRestore = await prisma.emailTemplateVersion.findUnique({
where: {
templateId_versionNumber: {
templateId: id,
versionNumber,
},
},
});
if (!versionToRestore) {
return res.status(404).json({ error: 'Version not found' });
}
// 2. Create version snapshot BEFORE rollback (current state)
await createVersion(id, {
changeNotes: `Rolled back to version ${versionNumber}`,
createdByUserId: req.user!.id,
});
// 3. Update template with old version content
const updatedTemplate = await prisma.emailTemplate.update({
where: { id },
data: {
subjectLine: versionToRestore.subjectLine,
htmlContent: versionToRestore.htmlContent,
textContent: versionToRestore.textContent,
updatedByUserId: req.user!.id,
},
});
res.json(updatedTemplate);
} catch (error) {
logger.error('Failed to rollback template', { error, templateId: id, versionNumber });
res.status(500).json({ error: 'Failed to rollback template' });
}
});
Important: Rollback is non-destructive. It doesn't delete newer versions; it creates a NEW version with old content. This preserves the complete audit trail.
Code Examples
Creating Version Manually
When to Use:
- Seed script initialization
- Programmatic template updates
- Testing version history
Example:
// Create initial version for new template
const template = await prisma.emailTemplate.create({
data: {
key: 'user-welcome',
name: 'Welcome Email',
category: 'SYSTEM',
subjectLine: 'Welcome!',
htmlContent: '<p>Hello {{USER_NAME}}</p>',
textContent: 'Hello {{USER_NAME}}',
isActive: true,
},
});
// Create version 1
await prisma.emailTemplateVersion.create({
data: {
templateId: template.id,
versionNumber: 1,
subjectLine: template.subjectLine,
htmlContent: template.htmlContent,
textContent: template.textContent,
changeNotes: 'Initial template creation',
createdByUserId: adminUser.id,
},
});
Loading Version History
Fetch All Versions:
const versions = await prisma.emailTemplateVersion.findMany({
where: { templateId },
orderBy: { versionNumber: 'desc' },
include: {
createdBy: {
select: { name: true, email: true },
},
},
});
console.log('Version history:', versions);
Fetch Specific Version:
const version = await prisma.emailTemplateVersion.findUnique({
where: {
templateId_versionNumber: {
templateId: 'cuid123',
versionNumber: 5,
},
},
});
Fetch Latest Version:
const latestVersion = await prisma.emailTemplateVersion.findFirst({
where: { templateId },
orderBy: { versionNumber: 'desc' },
});
console.log('Latest version:', latestVersion.versionNumber);
Version Diff Generation
Line-by-Line Diff:
import { diffLines, Change } from 'diff';
interface VersionDiff {
subject: Change[];
html: Change[];
text: Change[];
}
function compareVersions(
oldVersion: EmailTemplateVersion,
newVersion: EmailTemplateVersion
): VersionDiff {
return {
subject: diffLines(oldVersion.subjectLine, newVersion.subjectLine),
html: diffLines(oldVersion.htmlContent, newVersion.htmlContent),
text: diffLines(oldVersion.textContent, newVersion.textContent),
};
}
Usage:
const version5 = await prisma.emailTemplateVersion.findUnique({
where: { templateId_versionNumber: { templateId, versionNumber: 5 } },
});
const version6 = await prisma.emailTemplateVersion.findUnique({
where: { templateId_versionNumber: { templateId, versionNumber: 6 } },
});
const diff = compareVersions(version5, version6);
console.log('Subject changes:', diff.subject);
console.log('HTML changes:', diff.html);
console.log('Text changes:', diff.text);
Render Diff in UI:
// admin/src/components/VersionDiff.tsx
import { diffLines } from 'diff';
interface VersionDiffProps {
oldContent: string;
newContent: string;
title: string;
}
export function VersionDiff({ oldContent, newContent, title }: VersionDiffProps) {
const diff = diffLines(oldContent, newContent);
return (
<div>
<h4>{title}</h4>
<pre style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace', fontSize: 12 }}>
{diff.map((part, index) => {
let style = {};
if (part.added) {
style = { color: 'green', backgroundColor: '#e6ffed' };
} else if (part.removed) {
style = { color: 'red', backgroundColor: '#ffebe9' };
}
return (
<span key={index} style={style}>
{part.value}
</span>
);
})}
</pre>
</div>
);
}
Rollback API Implementation
Full Rollback Route:
// api/src/modules/email-templates/email-templates.routes.ts
import { Router } from 'express';
import { requireRole } from '@/middleware/auth';
import { prisma } from '@/config/database';
import { logger } from '@/utils/logger';
const router = Router();
router.post('/:id/rollback/:versionNumber', requireRole('SUPER_ADMIN'), async (req, res) => {
const { id } = req.params;
const versionNumber = parseInt(req.params.versionNumber, 10);
if (isNaN(versionNumber) || versionNumber < 1) {
return res.status(400).json({ error: 'Invalid version number' });
}
try {
// 1. Load version to restore
const versionToRestore = await prisma.emailTemplateVersion.findUnique({
where: {
templateId_versionNumber: {
templateId: id,
versionNumber,
},
},
});
if (!versionToRestore) {
return res.status(404).json({ error: 'Version not found' });
}
// 2. Load current template
const currentTemplate = await prisma.emailTemplate.findUnique({
where: { id },
});
if (!currentTemplate) {
return res.status(404).json({ error: 'Template not found' });
}
// 3. Check if already at this version (no-op)
if (
currentTemplate.subjectLine === versionToRestore.subjectLine &&
currentTemplate.htmlContent === versionToRestore.htmlContent &&
currentTemplate.textContent === versionToRestore.textContent
) {
return res.status(400).json({ error: 'Template already matches this version' });
}
// 4. Use transaction for atomicity
await prisma.$transaction(async (tx) => {
// 4a. Create version snapshot of CURRENT state
const maxVersion = await tx.emailTemplateVersion.findFirst({
where: { templateId: id },
orderBy: { versionNumber: 'desc' },
select: { versionNumber: true },
});
const nextVersion = (maxVersion?.versionNumber || 0) + 1;
await tx.emailTemplateVersion.create({
data: {
templateId: id,
versionNumber: nextVersion,
subjectLine: currentTemplate.subjectLine,
htmlContent: currentTemplate.htmlContent,
textContent: currentTemplate.textContent,
changeNotes: `Rolled back to version ${versionNumber}`,
createdByUserId: req.user!.id,
},
});
// 4b. Update template with OLD version content
await tx.emailTemplate.update({
where: { id },
data: {
subjectLine: versionToRestore.subjectLine,
htmlContent: versionToRestore.htmlContent,
textContent: versionToRestore.textContent,
updatedByUserId: req.user!.id,
},
});
});
// 5. Load updated template
const updatedTemplate = await prisma.emailTemplate.findUnique({
where: { id },
});
logger.info('Template rolled back', {
templateId: id,
toVersion: versionNumber,
userId: req.user!.id,
});
res.json(updatedTemplate);
} catch (error: any) {
logger.error('Failed to rollback template', {
error: error.message,
templateId: id,
versionNumber,
});
res.status(500).json({ error: 'Failed to rollback template' });
}
});
export default router;
Troubleshooting
Problem: Version numbers not auto-incrementing
Symptoms:
- Duplicate version number error
P2002: Unique constraint failed on templateId_versionNumber
Causes:
- Race condition (two saves at same time)
- Max version query returns wrong result
- Database constraint violated
Solutions:
Check max version:
SELECT MAX(version_number) FROM email_template_versions
WHERE template_id = 'cuid123';
Use transaction for atomicity:
await prisma.$transaction(async (tx) => {
// 1. Find max version
const maxVersion = await tx.emailTemplateVersion.findFirst({
where: { templateId },
orderBy: { versionNumber: 'desc' },
});
const nextVersion = (maxVersion?.versionNumber || 0) + 1;
// 2. Create version (within same transaction)
await tx.emailTemplateVersion.create({
data: {
templateId,
versionNumber: nextVersion,
// ...
},
});
});
Reset sequence if needed:
-- Check for gaps
SELECT version_number FROM email_template_versions
WHERE template_id = 'cuid123'
ORDER BY version_number;
-- If gaps exist, renumber (DANGEROUS, only in dev)
UPDATE email_template_versions
SET version_number = (
SELECT COUNT(*) FROM email_template_versions AS v2
WHERE v2.template_id = email_template_versions.template_id
AND v2.created_at <= email_template_versions.created_at
)
WHERE template_id = 'cuid123';
Problem: Rollback creates infinite versions
Symptoms:
- Rollback triggers another rollback
- Version numbers increment rapidly
Causes:
- Rollback doesn't use transaction
- Version creation triggers template update hook
Solutions:
Use atomic transaction:
await prisma.$transaction(async (tx) => {
// Create version + update template in same transaction
await tx.emailTemplateVersion.create({ ... });
await tx.emailTemplate.update({ ... });
});
Disable hooks during rollback:
// If using Prisma middleware, skip version creation during rollback
prisma.$use(async (params, next) => {
if (params.model === 'EmailTemplate' && params.action === 'update') {
// Check if this is a rollback operation (via context flag)
if (params.args.data._isRollback) {
delete params.args.data._isRollback;
return next(params); // Skip version creation
}
// Normal update: create version
await createVersion(params.args.where.id);
}
return next(params);
});
Problem: Version history shows duplicate content
Symptoms:
- Multiple versions with identical content
- Version numbers increment but content unchanged
Causes:
- Save triggered multiple times (double-click)
- No dirty check before saving
Solutions:
Add content comparison before save:
router.put('/:id', requireRole(SUPER_ADMIN), async (req, res) => {
const { id } = req.params;
const { subjectLine, htmlContent, textContent, changeNotes } = req.body;
// 1. Load current template
const currentTemplate = await prisma.emailTemplate.findUnique({ where: { id } });
// 2. Check if content changed
if (
currentTemplate.subjectLine === subjectLine &&
currentTemplate.htmlContent === htmlContent &&
currentTemplate.textContent === textContent
) {
return res.status(400).json({ error: 'No changes detected' });
}
// 3. Create version + update template
await createVersion(id, { changeNotes, createdByUserId: req.user!.id });
await prisma.emailTemplate.update({ where: { id }, data: { subjectLine, htmlContent, textContent } });
res.json({ success: true });
});
Debounce save button:
// EmailTemplateEditorPage.tsx
const [saving, setSaving] = useState(false);
const handleSave = async () => {
if (saving) return; // Prevent double-click
setSaving(true);
try {
await api.put(`/api/email-templates/${id}`, { ... });
} finally {
setSaving(false);
}
};
Problem: Version comparison shows no diff
Symptoms:
- Comparison modal shows identical content
- No green/red highlighting
Causes:
- Comparing version with itself
- Versions truly identical (duplicate save)
Solutions:
Prevent self-comparison:
function handleCompare(version1: number, version2: number) {
if (version1 === version2) {
message.error('Cannot compare version with itself');
return;
}
// Load and compare versions...
}
Check versions exist:
SELECT version_number, LENGTH(html_content) AS html_length
FROM email_template_versions
WHERE template_id = 'cuid123'
AND version_number IN (5, 6);
Problem: Rollback doesn't restore variables
Symptoms:
- Template content rolled back
- Variables not restored (still showing new variables)
Causes:
- Variables stored separately (not in version snapshot)
Current Limitation:
- EmailTemplateVersion only stores content (subject, HTML, text)
- Does NOT store variable definitions
- Rolling back template doesn't affect variables
Workaround:
- Manually restore variables via admin UI
- Future enhancement: Version variable definitions too
Future Enhancement:
// Add to EmailTemplateVersion model
variablesSnapshot: Prisma.JsonValue // JSON array of variables
// When creating version, snapshot variables
const variables = await prisma.emailTemplateVariable.findMany({
where: { templateId },
});
await prisma.emailTemplateVersion.create({
data: {
// ...
variablesSnapshot: variables as unknown as Prisma.InputJsonValue,
},
});
// When rolling back, restore variables
const variablesSnapshot = versionToRestore.variablesSnapshot as EmailTemplateVariable[];
for (const variable of variablesSnapshot) {
await prisma.emailTemplateVariable.upsert({
where: { templateId_key: { templateId, key: variable.key } },
update: variable,
create: { templateId, ...variable },
});
}
Performance Considerations
Version Storage Growth
Storage Impact:
- Each version stores 3 text fields (subject, HTML, text)
- Typical template: 5-20KB per version
- 100 versions = 500KB - 2MB per template
Optimization Options:
1. Compress Old Versions:
import zlib from 'zlib';
// Compress HTML content before storing
const compressedHtml = zlib.gzipSync(htmlContent).toString('base64');
await prisma.emailTemplateVersion.create({
data: {
// ...
htmlContent: compressedHtml,
isCompressed: true, // Add flag
},
});
// Decompress when loading
if (version.isCompressed) {
const buffer = Buffer.from(version.htmlContent, 'base64');
const htmlContent = zlib.gunzipSync(buffer).toString('utf-8');
}
2. Archive Old Versions:
-- Move versions > 1 year old to archive table
INSERT INTO email_template_versions_archive
SELECT * FROM email_template_versions
WHERE created_at < NOW() - INTERVAL '1 year';
DELETE FROM email_template_versions
WHERE created_at < NOW() - INTERVAL '1 year';
3. Limit Version History:
// Keep only last 50 versions per template
const oldVersions = await prisma.emailTemplateVersion.findMany({
where: { templateId },
orderBy: { versionNumber: 'desc' },
skip: 50, // Skip first 50 (keep these)
});
// Delete versions beyond 50
await prisma.emailTemplateVersion.deleteMany({
where: {
id: { in: oldVersions.map(v => v.id) },
},
});
Version Diff Performance
Performance Impact:
- Diff generation is CPU-intensive for large templates
diffLinesalgorithm is O(n*m) where n, m = line counts
Optimization:
1. Cache Diff Results:
const diffCache = new Map<string, Change[]>();
function getCachedDiff(oldContent: string, newContent: string): Change[] {
const cacheKey = `${hashString(oldContent)}-${hashString(newContent)}`;
if (diffCache.has(cacheKey)) {
return diffCache.get(cacheKey)!;
}
const diff = diffLines(oldContent, newContent);
diffCache.set(cacheKey, diff);
return diff;
}
2. Limit Diff Size:
// For very large templates, show summary instead of full diff
if (oldContent.length > 100000 || newContent.length > 100000) {
return {
error: 'Template too large for diff. Use version preview instead.',
};
}
Best Practices
Change Notes Guidelines
Always Provide Change Notes:
- Documents WHY changes were made (not just WHAT)
- Helps future admins understand context
- Useful for compliance audits
Be Specific:
✓ Good:
- "Added USER_PHONE variable with conditional block"
- "Fixed typo in subject line (Welcome vs Welcom)"
- "Updated shift location to include full address"
✗ Bad:
- "updates"
- "changes"
- "fix"
Reference Issues/Tickets:
"Fixed rendering issue in Gmail (Ticket #123)"
"Added new variable per Sarah's request"
Rollback Safety
Always Review Before Rollback:
- View version content before restoring
- Compare with current version
- Understand what will change
Use Change Notes:
"Rolled back to version 5 - version 6 broke email rendering in Outlook"
Test After Rollback:
- Send test email after rollback
- Verify rendering correct
- Check all variables still work
Version Retention
Keep All Versions (Default):
- Complete audit trail
- Compliance requirements
Archive Old Versions (Optional):
- Templates with 100+ versions
- Versions older than 1 year
- Move to separate archive table
Never Delete Versions:
- Breaks audit trail
- May violate compliance requirements
- Disk space is cheap
Related Documentation
Frontend Documentation
- EmailTemplatesPage.tsx — Version history tab
- EmailTemplateEditorPage.tsx — Change notes field
Backend Documentation
- Email Templates Module — Version API routes
GET /api/email-templates/:id/versions— List versionsGET /api/email-templates/:id/versions/:versionNumber— Get version detailsPOST /api/email-templates/:id/rollback/:versionNumber— Rollback to version
Database Documentation
- Email Templates Models — EmailTemplateVersion schema
Feature Documentation
- template-system.md — Email template engine overview
- editor.md — Email template editor interface
- variables.md — Template variable system