1234 lines
31 KiB
Markdown

# 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
```mermaid
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:**
```prisma
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:**
```typescript
// 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:**
```typescript
// 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:**
```
- "update" (not descriptive)
- "changes" (too vague)
- "" (empty, no context)
```
**Implementation:**
```typescript
// 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:**
```typescript
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:**
```typescript
// 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:**
```typescript
// 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:**
```typescript
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:**
```typescript
const version = await prisma.emailTemplateVersion.findUnique({
where: {
templateId_versionNumber: {
templateId: 'cuid123',
versionNumber: 5,
},
},
});
```
**Fetch Latest Version:**
```typescript
const latestVersion = await prisma.emailTemplateVersion.findFirst({
where: { templateId },
orderBy: { versionNumber: 'desc' },
});
console.log('Latest version:', latestVersion.versionNumber);
```
---
### Version Diff Generation
**Line-by-Line Diff:**
```typescript
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:**
```typescript
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:**
```typescript
// 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:**
```typescript
// 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:**
```sql
SELECT MAX(version_number) FROM email_template_versions
WHERE template_id = 'cuid123';
```
**Use transaction for atomicity:**
```typescript
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:**
```sql
-- 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:**
```typescript
await prisma.$transaction(async (tx) => {
// Create version + update template in same transaction
await tx.emailTemplateVersion.create({ ... });
await tx.emailTemplate.update({ ... });
});
```
**Disable hooks during rollback:**
```typescript
// 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:**
```typescript
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:**
```typescript
// 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:**
```typescript
function handleCompare(version1: number, version2: number) {
if (version1 === version2) {
message.error('Cannot compare version with itself');
return;
}
// Load and compare versions...
}
```
**Check versions exist:**
```sql
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:**
```typescript
// 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:**
```typescript
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:**
```sql
-- 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:**
```typescript
// 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:**
```typescript
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:**
```typescript
// 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](../../frontend/pages/email-templates-page.md)** — Version history tab
- **[EmailTemplateEditorPage.tsx](../../frontend/pages/email-template-editor-page.md)** — Change notes field
### Backend Documentation
- **[Email Templates Module](../../api/modules/email-templates.md)** — Version API routes
- `GET /api/email-templates/:id/versions` — List versions
- `GET /api/email-templates/:id/versions/:versionNumber` — Get version details
- `POST /api/email-templates/:id/rollback/:versionNumber` — Rollback to version
### Database Documentation
- **[Email Templates Models](../../database/models/email-templates.md)** — EmailTemplateVersion schema
### Feature Documentation
- **[template-system.md](./template-system.md)** — Email template engine overview
- **[editor.md](./editor.md)** — Email template editor interface
- **[variables.md](./variables.md)** — Template variable system