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/:id endpoint 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 changeNotes field
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:
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: 1. Race condition (two saves at same time) 2. Max version query returns wrong result 3. Database constraint violated
Solutions:
Check max version:
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: 1. Rollback doesn't use transaction 2. 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: 1. Save triggered multiple times (double-click) 2. 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: 1. Comparing version with itself 2. 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
- diffLines algorithm 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:
Rollback Safety¶
Always Review Before Rollback: - View version content before restoring - Compare with current version - Understand what will change
Use Change Notes:
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