33 KiB
Template Variables System
Overview
The Template Variables System defines reusable placeholders for email templates, enabling dynamic content interpolation with validation, documentation, and sample values. Variables are defined per template and provide metadata for variable insertion UI, validation logic, and testing workflows.
Key Features:
- Per-Template Variables — Each template has its own variable definitions
- Required vs Optional — Enforce required variables at runtime
- Conditional Variables — Boolean/truthy flags for
{{#if}}blocks - Sample Values — Example data for testing and preview
- Sort Order — Control display order in editor UI
- Documentation — Labels and descriptions for self-documenting templates
- Validation — Runtime checks prevent missing variable errors
- Reusability — Common variables (USER_NAME, USER_EMAIL) across templates
Benefits:
- Type Safety — Know what data is expected before sending
- Self-Documentation — Variables describe their purpose
- Better Testing — Sample values pre-fill test send forms
- Consistency — Standardized variable naming across templates
- Error Prevention — Catch missing variables before SMTP send
Architecture
flowchart TB
subgraph "Database Layer"
Template[(EmailTemplate)]
Variables[(EmailTemplateVariable)]
Template -->|1:N| Variables
end
subgraph "Variable Definition"
VarKey[Variable Key<br/>USER_NAME]
VarMeta[Metadata<br/>label, description, isRequired]
VarSample[Sample Value<br/>'John Doe']
VarSort[Sort Order<br/>1, 2, 3...]
VarKey --> Variables
VarMeta --> Variables
VarSample --> Variables
VarSort --> Variables
end
subgraph "Template Service"
Load[Load Template + Variables]
Validate[Validate Required Variables]
Interpolate[Handlebars Interpolation]
Load --> Template
Load --> Variables
Validate --> Variables
Interpolate -->|{{VAR}}| Data[Data Object]
end
subgraph "Editor UI"
InsertPanel[Variable Insertion Panel]
PreviewForm[Sample Data Form]
TestSend[Test Send Form]
Variables --> InsertPanel
Variables --> PreviewForm
Variables --> TestSend
end
subgraph "Runtime Validation"
Send[Send Email]
Check{Required<br/>Variables<br/>Present?}
Error[Throw MissingVariableError]
Success[Send via SMTP]
Send --> Validate
Validate --> Check
Check -->|No| Error
Check -->|Yes| Interpolate
Interpolate --> Success
end
style Template fill:#50c878,color:#fff
style Variables fill:#4a90e2,color:#fff
style Validate fill:#ffb347,color:#333
Component Responsibilities:
- EmailTemplateVariable — Database model storing variable metadata
- Variable Insertion Panel — Editor UI for inserting
{{VARIABLES}} - Sample Data Form — Preview/test form pre-filled with sample values
- Validation Service — Runtime checks before template interpolation
- Handlebars Engine — Replaces
{{VAR}}with data values
Database Model
EmailTemplateVariable Schema
Table: email_template_variables
| Field | Type | Description |
|---|---|---|
id |
String (CUID) | Primary key |
templateId |
String | Foreign key to EmailTemplate |
key |
String | Variable name (UPPERCASE_WITH_UNDERSCORES) |
label |
String | Display label for UI ("User's Full Name") |
description |
String (optional) | Variable purpose and usage notes |
isRequired |
Boolean | If true, must be provided in data object |
isConditional |
Boolean | If true, used in {{#if}} blocks (truthy/falsy) |
sampleValue |
String (optional) | Example value for testing/preview |
sortOrder |
Int | Display order in UI (1, 2, 3...) |
createdAt |
DateTime | Creation timestamp |
Relations:
template— EmailTemplate (N:1)
Constraints:
- Unique index on
(templateId, key)— prevents duplicate variables per template - Index on
sortOrderfor ordered queries
Prisma Schema:
model EmailTemplateVariable {
id String @id @default(cuid())
templateId String
key String
label String
description String?
isRequired Boolean @default(false)
isConditional Boolean @default(false)
sampleValue String?
sortOrder Int @default(0)
createdAt DateTime @default(now())
template EmailTemplate @relation(fields: [templateId], references: [id], onDelete: Cascade)
@@unique([templateId, key])
@@index([sortOrder])
@@map("email_template_variables")
}
Variable Types
Required Variables
Purpose: Must be provided in data object for template to send.
Behavior:
- Validation checks for presence before interpolation
- Throws
MissingRequiredVariableErrorif missing - Marked with red "Required" badge in editor UI
When to Use:
- Variables that appear in ALL template renders
- Variables without fallback values
- Critical data (e.g., recipient name, event date)
Example:
await prisma.emailTemplateVariable.create({
data: {
templateId: template.id,
key: 'USER_NAME',
label: 'User Name',
description: 'Full name of the email recipient',
isRequired: true, // ← MUST be provided
isConditional: false,
sampleValue: 'John Doe',
sortOrder: 1,
},
});
Template Usage:
<p>Dear {{USER_NAME}},</p>
<!-- USER_NAME is required, error if missing -->
Optional Variables
Purpose: May be omitted from data object (defaults to empty string).
Behavior:
- No validation error if missing
- Handlebars renders as empty string if undefined
- Useful for conditional content or nice-to-have data
When to Use:
- Variables that may not always be available (e.g., phone number)
- Variables with fallback text in template
- Conditional blocks that check presence
Example:
await prisma.emailTemplateVariable.create({
data: {
templateId: template.id,
key: 'USER_PHONE',
label: 'User Phone',
description: 'User phone number (optional)',
isRequired: false, // ← Can be omitted
isConditional: false,
sampleValue: '(555) 123-4567',
sortOrder: 5,
},
});
Template Usage:
{{#if USER_PHONE}}
<p>We'll call you at {{USER_PHONE}}.</p>
{{else}}
<p>Add a phone number to receive SMS updates.</p>
{{/if}}
Conditional Variables
Purpose: Boolean or truthy/falsy values for {{#if}} blocks.
Behavior:
isConditional: truemarks variable as boolean-like- Editor UI shows blue "Conditional" badge
- Used in
{{#if VAR}}...{{/if}}blocks - Can also be required or optional
When to Use:
- Boolean flags (HAS_PHONE, IS_VERIFIED, IS_ADMIN)
- Existence checks (HAS_CUSTOM_MESSAGE, HAS_LOCATION)
- Feature flags (SHOW_DISCOUNT, SHOW_MAP_LINK)
Example:
await prisma.emailTemplateVariable.create({
data: {
templateId: template.id,
key: 'HAS_PHONE',
label: 'Has Phone Number',
description: 'Whether user provided a phone number',
isRequired: false,
isConditional: true, // ← Boolean/truthy variable
sampleValue: 'true',
sortOrder: 4,
},
});
Template Usage:
{{#if HAS_PHONE}}
<p>Contact: {{USER_PHONE}}</p>
{{/if}}
Truthy Values:
true,'true',1, non-empty strings, non-empty arrays
Falsy Values:
false,'false',0,'',null,undefined,[]
Array Variables (Loops)
Purpose: Collections for {{#each}} blocks.
Behavior:
- Not explicitly marked (same as other variables)
- Sample value should be JSON array string
- Used in
{{#each VAR}}...{{/each}}loops
When to Use:
- Lists of representatives, shift assignments, visit outcomes
- Dynamic content length (1-N items)
Example:
await prisma.emailTemplateVariable.create({
data: {
templateId: template.id,
key: 'REPRESENTATIVES',
label: 'Representative List',
description: 'Array of representative objects',
isRequired: true,
isConditional: false,
sampleValue: JSON.stringify([
{ name: 'Jane Doe', title: 'MP', email: 'jane@parliament.ca' },
{ name: 'John Smith', title: 'Councillor', email: 'john@city.ca' },
]),
sortOrder: 10,
},
});
Template Usage:
<ul>
{{#each REPRESENTATIVES}}
<li>
<strong>{{name}}</strong> ({{title}})<br>
Email: {{email}}
</li>
{{/each}}
</ul>
Data Object:
{
REPRESENTATIVES: [
{ name: 'Jane Doe', title: 'MP', email: 'jane@parliament.ca' },
{ name: 'John Smith', title: 'Councillor', email: 'john@city.ca' },
],
}
Admin Workflow
Viewing Variables
From EmailTemplatesPage:
-
Click Template Row
- Opens template detail modal
-
Navigate to "Variables" Tab
- Shows table of all variables
- Columns: Key, Label, Required, Conditional, Sample Value, Sort Order
-
Variable Details
- Click variable row for description
- See where variable is used in template content
- View sample value
From EmailTemplateEditorPage:
-
Open Template Editor
- Variables shown in right sidebar
-
Variable Insertion Panel
- Variables listed with labels, badges, descriptions
- Sorted by sortOrder ascending
- Click "Insert to HTML/Text" buttons
Adding Variable
Step 1: Open Variables Tab
- EmailTemplatesPage → click template → "Variables" tab
Step 2: Click "Add Variable" Button
- Opens variable creation modal
Step 3: Enter Variable Metadata
Key (required):
- Uppercase with underscores (e.g.,
USER_NAME) - Must be unique within template
- Used in template as
{{KEY}}
Label (required):
- Display name for UI (e.g., "User's Full Name")
- Human-readable description
Description (optional):
- Detailed explanation of variable purpose
- Usage notes (e.g., "Must be in YYYY-MM-DD format")
Is Required:
- Toggle on if variable must always be provided
- Validation will fail if missing
Is Conditional:
- Toggle on if variable is used in
{{#if}}blocks - UI shows blue "Conditional" badge
Sample Value (optional):
- Example value for testing/preview
- Pre-fills test send form
- Shows expected data format
Sort Order:
- Numeric order for UI display
- Lower numbers appear first (1, 2, 3...)
- Auto-assigned if not specified
Step 4: Save Variable
- Click "Save" button
- Variable added to template
- Available in editor insertion panel
Editing Variable
Step 1: Open Variables Tab
- EmailTemplatesPage → click template → "Variables" tab
Step 2: Click Variable Row
- Opens variable edit modal
- Shows current values
Step 3: Modify Fields
- Change label, description, flags, sample value
- Cannot change key (would break existing templates)
Step 4: Save Changes
- Click "Save" button
- Variable updated in database
Note: Changing variable key requires creating new variable and updating template content manually.
Deleting Variable
Step 1: Check Template Usage
- Search template content for
{{VAR_KEY}} - Ensure variable is not used in subject/HTML/text
Step 2: Click Delete Button
- Variables tab → click variable row → "Delete" button
Step 3: Confirm Deletion
- Warning modal: "Are you sure? This cannot be undone."
- Click "Confirm Delete"
Step 4: Verify Template Still Valid
- Open template editor
- Check preview renders without errors
- Send test email
Warning: Deleting a variable that's still used in template content will cause rendering errors ({{VAR}} will appear as literal text).
Reordering Variables
Step 1: Open Variables Tab
- EmailTemplatesPage → click template → "Variables" tab
Step 2: Drag to Reorder
- Drag variable rows up/down
- Drop to new position
Step 3: Save Sort Order
- Click "Save Order" button
- Updates
sortOrderfield for all variables
Alternative: Manual Sort Order
- Edit variable → change sortOrder number
- Variables re-sort automatically
Developer Workflow
Creating Variables Programmatically
Seed Script Example:
// api/prisma/seed.ts
const template = await prisma.emailTemplate.findUnique({
where: { key: 'shift-signup-confirmation' },
});
if (!template) throw new Error('Template not found');
const variables = [
{
key: 'USER_NAME',
label: 'User Name',
description: 'Full name of the volunteer',
isRequired: true,
isConditional: false,
sampleValue: 'John Doe',
sortOrder: 1,
},
{
key: 'USER_EMAIL',
label: 'User Email',
description: 'Email address of the volunteer',
isRequired: true,
isConditional: false,
sampleValue: 'john@example.com',
sortOrder: 2,
},
{
key: 'SHIFT_TITLE',
label: 'Shift Title',
description: 'Name of the shift',
isRequired: true,
isConditional: false,
sampleValue: 'Door Knocking - Downtown',
sortOrder: 3,
},
{
key: 'SHIFT_DATE',
label: 'Shift Date',
description: 'Formatted shift date (e.g., "Saturday, March 15, 2026")',
isRequired: true,
isConditional: false,
sampleValue: 'Saturday, March 15, 2026',
sortOrder: 4,
},
{
key: 'SHIFT_TIME',
label: 'Shift Time',
description: 'Shift time range (e.g., "10:00 AM - 2:00 PM")',
isRequired: true,
isConditional: false,
sampleValue: '10:00 AM - 2:00 PM',
sortOrder: 5,
},
{
key: 'SHIFT_LOCATION',
label: 'Shift Location',
description: 'Meeting location for shift',
isRequired: true,
isConditional: false,
sampleValue: 'Campaign Office (123 Main St)',
sortOrder: 6,
},
{
key: 'HAS_PHONE',
label: 'Has Phone Number',
description: 'Whether user provided a phone number',
isRequired: false,
isConditional: true,
sampleValue: 'true',
sortOrder: 7,
},
{
key: 'USER_PHONE',
label: 'User Phone',
description: 'User phone number (optional)',
isRequired: false,
isConditional: false,
sampleValue: '(555) 123-4567',
sortOrder: 8,
},
];
// Upsert variables
for (const variable of variables) {
await prisma.emailTemplateVariable.upsert({
where: {
templateId_key: {
templateId: template.id,
key: variable.key,
},
},
update: {
label: variable.label,
description: variable.description,
isRequired: variable.isRequired,
isConditional: variable.isConditional,
sampleValue: variable.sampleValue,
sortOrder: variable.sortOrder,
},
create: {
templateId: template.id,
...variable,
},
});
}
console.log(`✓ Created ${variables.length} variables for shift-signup-confirmation template`);
Loading Variables in Code
With Template:
const template = await prisma.emailTemplate.findUnique({
where: { key: 'shift-signup-confirmation' },
include: { variables: true },
});
console.log('Template variables:', template?.variables);
Ordered by Sort:
const template = await prisma.emailTemplate.findUnique({
where: { key: 'shift-signup-confirmation' },
include: {
variables: {
orderBy: { sortOrder: 'asc' },
},
},
});
Required Variables Only:
const requiredVars = await prisma.emailTemplateVariable.findMany({
where: {
templateId: template.id,
isRequired: true,
},
});
console.log('Required variables:', requiredVars.map(v => v.key));
Validating Variables
Validation Function:
// api/src/services/email.service.ts
function validateVariables(
template: EmailTemplate & { variables: EmailTemplateVariable[] },
data: Record<string, unknown>
) {
const missing: string[] = [];
for (const variable of template.variables) {
if (variable.isRequired && (data[variable.key] === undefined || data[variable.key] === null)) {
missing.push(variable.key);
}
}
if (missing.length > 0) {
throw new MissingRequiredVariableError(
`Missing required variables for template ${template.key}: ${missing.join(', ')}`
);
}
}
Usage:
const template = await prisma.emailTemplate.findUnique({
where: { key: 'shift-reminder' },
include: { variables: true },
});
try {
validateVariables(template, {
USER_NAME: 'John Doe',
SHIFT_DATE: '2026-03-15',
// Missing SHIFT_TITLE (required)
});
} catch (error) {
console.error('Validation failed:', error.message);
// Error: Missing required variables for template shift-reminder: SHIFT_TITLE
}
Code Examples
Creating Variable via API
Endpoint: POST /api/email-templates/:id/variables
Request Body:
{
"key": "USER_NAME",
"label": "User Name",
"description": "Full name of the email recipient",
"isRequired": true,
"isConditional": false,
"sampleValue": "John Doe",
"sortOrder": 1
}
Route Implementation:
// api/src/modules/email-templates/email-templates.routes.ts
router.post('/:id/variables', requireRole(SUPER_ADMIN), async (req, res) => {
const { id } = req.params;
const { key, label, description, isRequired, isConditional, sampleValue, sortOrder } = req.body;
try {
const variable = await prisma.emailTemplateVariable.create({
data: {
templateId: id,
key,
label,
description,
isRequired: isRequired || false,
isConditional: isConditional || false,
sampleValue,
sortOrder: sortOrder || 0,
},
});
res.json(variable);
} catch (error: any) {
if (error.code === 'P2002') {
// Unique constraint violation
return res.status(400).json({ error: 'Variable key already exists for this template' });
}
throw error;
}
});
Auto-Generating Sample Data
Load Sample Data from Variables:
function generateSampleData(variables: EmailTemplateVariable[]): Record<string, unknown> {
const sampleData: Record<string, unknown> = {};
for (const variable of variables) {
if (variable.sampleValue) {
// Try to parse as JSON (for arrays/objects)
try {
sampleData[variable.key] = JSON.parse(variable.sampleValue);
} catch {
// Use as string
sampleData[variable.key] = variable.sampleValue;
}
} else if (variable.isConditional) {
// Default conditional variables to true
sampleData[variable.key] = true;
} else {
// Default to empty string
sampleData[variable.key] = '';
}
}
return sampleData;
}
Usage in Editor:
const template = await api.get(`/api/email-templates/${id}`);
const sampleData = generateSampleData(template.variables);
setSampleData(sampleData);
Variable Usage Detection
Find Variables Used in Template Content:
function findUsedVariables(content: string): string[] {
// Regex: matches {{VAR}} but not {{#if}}, {{/if}}, {{#each}}, etc.
const regex = /\{\{(?!#|\/|\^)([A-Z_]+)\}\}/g;
const matches = content.matchAll(regex);
const variables = new Set<string>();
for (const match of matches) {
variables.add(match[1]);
}
return Array.from(variables);
}
Check for Unused Variables:
const template = await prisma.emailTemplate.findUnique({
where: { id: templateId },
include: { variables: true },
});
const htmlVars = findUsedVariables(template.htmlContent);
const textVars = findUsedVariables(template.textContent);
const subjectVars = findUsedVariables(template.subjectLine);
const usedVars = new Set([...htmlVars, ...textVars, ...subjectVars]);
const unusedVars = template.variables.filter(v => !usedVars.has(v.key));
console.log('Unused variables:', unusedVars.map(v => v.key));
Common Variables by Category
INFLUENCE Templates
Standard Variables:
| Key | Label | Required | Conditional | Description |
|---|---|---|---|---|
USER_NAME |
User Name | Yes | No | Participant's full name |
USER_EMAIL |
User Email | Yes | No | Participant's email address |
CAMPAIGN_TITLE |
Campaign Title | Yes | No | Campaign name |
CAMPAIGN_SLUG |
Campaign Slug | Yes | No | URL-safe campaign identifier |
CAMPAIGN_URL |
Campaign URL | No | No | Full URL to campaign page |
REPRESENTATIVE_NAME |
Representative Name | Yes | No | Representative's full name |
REPRESENTATIVE_TITLE |
Representative Title | Yes | No | Representative's title (e.g., "MP for Downtown") |
REPRESENTATIVE_EMAIL |
Representative Email | Yes | No | Representative's email address |
CUSTOM_MESSAGE |
Custom Message | Yes | No | Participant's custom message to representative |
RESPONSE_TEXT |
Response Text | No | No | Participant's response wall submission |
VERIFICATION_LINK |
Verification Link | No | No | Unique verification URL |
HAS_CUSTOM_MESSAGE |
Has Custom Message | No | Yes | Whether participant added custom message |
Usage Example:
await emailService.sendFromTemplate('campaign-email', {
recipientEmail: representative.email,
data: {
USER_NAME: participant.name,
USER_EMAIL: participant.email,
CAMPAIGN_TITLE: campaign.title,
CAMPAIGN_SLUG: campaign.slug,
REPRESENTATIVE_NAME: representative.name,
REPRESENTATIVE_TITLE: representative.title,
REPRESENTATIVE_EMAIL: representative.email,
CUSTOM_MESSAGE: emailData.customMessage,
},
});
MAP Templates
Standard Variables:
| Key | Label | Required | Conditional | Description |
|---|---|---|---|---|
USER_NAME |
User Name | Yes | No | Volunteer's full name |
USER_EMAIL |
User Email | Yes | No | Volunteer's email address |
USER_PHONE |
User Phone | No | No | Volunteer's phone number (optional) |
HAS_PHONE |
Has Phone | No | Yes | Whether user provided phone number |
SHIFT_TITLE |
Shift Title | Yes | No | Shift name |
SHIFT_DATE |
Shift Date | Yes | No | Formatted shift date |
SHIFT_TIME |
Shift Time | Yes | No | Shift time range (e.g., "10:00 AM - 2:00 PM") |
SHIFT_LOCATION |
Shift Location | Yes | No | Meeting location for shift |
CUT_NAME |
Cut Name | No | No | Canvass area name |
IS_CUT_ASSIGNED |
Is Cut Assigned | No | Yes | Whether volunteer is assigned to a cut |
VISIT_COUNT |
Visit Count | No | No | Number of doors knocked (session summary) |
CONTACT_COUNT |
Contact Count | No | No | Number of successful contacts |
SUPPORT_COUNT |
Support Count | No | No | Number of supporters identified |
Usage Example:
await emailService.sendFromTemplate('shift-signup-confirmation', {
recipientEmail: volunteer.email,
data: {
USER_NAME: volunteer.name,
USER_EMAIL: volunteer.email,
USER_PHONE: volunteer.phone || '',
HAS_PHONE: !!volunteer.phone,
SHIFT_TITLE: shift.title,
SHIFT_DATE: dayjs(shift.startTime).format('dddd, MMMM D, YYYY'),
SHIFT_TIME: `${dayjs(shift.startTime).format('h:mm A')} - ${dayjs(shift.endTime).format('h:mm A')}`,
SHIFT_LOCATION: shift.location,
IS_CUT_ASSIGNED: !!shift.cutId,
CUT_NAME: shift.cut?.name || '',
},
});
SYSTEM Templates
Standard Variables:
| Key | Label | Required | Conditional | Description |
|---|---|---|---|---|
USER_NAME |
User Name | Yes | No | User's full name |
USER_EMAIL |
User Email | Yes | No | User's email address |
VERIFICATION_LINK |
Verification Link | No | No | Unique verification URL (expires 24h) |
RESET_LINK |
Reset Link | No | No | Unique password reset URL (expires 1h) |
SUPPORT_EMAIL |
Support Email | Yes | No | Platform support email address |
SITE_NAME |
Site Name | Yes | No | Platform name (from SiteSettings) |
SITE_URL |
Site URL | Yes | No | Platform base URL |
LOGIN_URL |
Login URL | No | No | Direct link to login page |
LOCKOUT_REASON |
Lockout Reason | No | No | Why account was locked (security) |
Usage Example:
await emailService.sendFromTemplate('password-reset', {
recipientEmail: user.email,
data: {
USER_NAME: user.name,
USER_EMAIL: user.email,
RESET_LINK: `https://cmlite.org/reset-password/${token}`,
SUPPORT_EMAIL: siteSettings.supportEmail,
SITE_NAME: siteSettings.siteName,
SITE_URL: siteSettings.siteUrl,
},
});
Troubleshooting
Problem: Variable not appearing in editor
Symptoms:
- Variable exists in database but not shown in editor insertion panel
- Variable missing from variables list
Causes:
- Variable belongs to different template
- Template not refreshed after adding variable
- Sort order is null or very high (out of view)
Solutions:
Check variable exists:
SELECT * FROM email_template_variables
WHERE template_id = 'cuid123' AND key = 'USER_NAME';
Verify template ID:
SELECT id, key FROM email_templates WHERE key = 'shift-reminder';
-- Check ID matches variable.template_id
Refresh editor page:
- Hard refresh (Ctrl+Shift+R)
- Clear browser cache
Check sort order:
SELECT key, sort_order FROM email_template_variables
WHERE template_id = 'cuid123'
ORDER BY sort_order;
-- Update if needed
UPDATE email_template_variables
SET sort_order = 1
WHERE id = 'variable-id';
Problem: Validation error for optional variable
Symptoms:
MissingRequiredVariableErrorthrown for variable marked as optional- Email send fails unexpectedly
Causes:
- Variable incorrectly marked as required in database
- Validation logic bug
- Template uses variable in required context
Solutions:
Check isRequired flag:
SELECT key, is_required FROM email_template_variables
WHERE key = 'USER_PHONE' AND template_id = 'cuid123';
Update to optional:
UPDATE email_template_variables
SET is_required = false
WHERE key = 'USER_PHONE' AND template_id = 'cuid123';
Provide variable anyway:
// Temporary fix: always provide optional variables
data: {
USER_PHONE: volunteer.phone || '', // Empty string if missing
}
Check validation logic:
// Ensure validation checks for undefined AND null
if (variable.isRequired && (data[variable.key] === undefined || data[variable.key] === null)) {
missing.push(variable.key);
}
Problem: Sample value not used in preview
Symptoms:
- Preview shows empty values instead of sample values
- Test send form doesn't pre-fill
Causes:
- Sample value is null in database
- Sample data initialization bug
- Variable added after editor loaded
Solutions:
Check sample value exists:
SELECT key, sample_value FROM email_template_variables
WHERE template_id = 'cuid123';
Update sample value:
UPDATE email_template_variables
SET sample_value = 'John Doe'
WHERE key = 'USER_NAME';
Refresh editor:
- Close and reopen EmailTemplateEditorPage
- Sample data reloads from variables
Manual preview data:
// Editor UI allows manual editing of sample data
setSampleData({
...sampleData,
USER_NAME: 'Test Name',
});
Problem: Duplicate variable key error
Symptoms:
P2002: Unique constraint failederror when creating variable- Cannot add variable with same key
Causes:
- Variable already exists for this template
- Attempting to create duplicate
Solutions:
Check existing variables:
SELECT * FROM email_template_variables
WHERE template_id = 'cuid123' AND key = 'USER_NAME';
Update existing instead:
await prisma.emailTemplateVariable.upsert({
where: {
templateId_key: {
templateId: template.id,
key: 'USER_NAME',
},
},
update: {
label: 'User Full Name', // Updated label
},
create: {
templateId: template.id,
key: 'USER_NAME',
label: 'User Full Name',
// ...
},
});
Use different key:
// If truly need separate variable
key: 'USER_FULL_NAME', // Not USER_NAME
Problem: Variables not alphabetically sorted
Symptoms:
- Variables appear in random order in editor
- Want alphabetical order instead of custom sort
Causes:
- Sort order not set alphabetically
- Need to update sortOrder values
Solutions:
Sort alphabetically by key:
-- Generate new sort order based on alphabetical order
WITH sorted AS (
SELECT id, ROW_NUMBER() OVER (PARTITION BY template_id ORDER BY key) AS new_order
FROM email_template_variables
WHERE template_id = 'cuid123'
)
UPDATE email_template_variables
SET sort_order = sorted.new_order
FROM sorted
WHERE email_template_variables.id = sorted.id;
Sort by label:
WITH sorted AS (
SELECT id, ROW_NUMBER() OVER (PARTITION BY template_id ORDER BY label) AS new_order
FROM email_template_variables
WHERE template_id = 'cuid123'
)
UPDATE email_template_variables
SET sort_order = sorted.new_order
FROM sorted
WHERE email_template_variables.id = sorted.id;
Manual custom order:
- Use admin UI to drag-drop reorder
- Saves custom sortOrder values
Performance Considerations
Variable Loading
Current Implementation:
- Variables loaded with template via
include: { variables: true } - Single database query (JOIN)
- Fast (< 10ms for typical templates)
Optimization for Many Variables:
// If template has 100+ variables, consider pagination
const variables = await prisma.emailTemplateVariable.findMany({
where: { templateId: template.id },
orderBy: { sortOrder: 'asc' },
take: 50, // Load first 50
skip: 0, // Offset for pagination
});
Validation Performance
Required Variable Check:
- O(n) where n = number of required variables
- Fast for typical templates (< 10 required vars)
- No database queries (uses in-memory variable list)
Caching Variables:
// Cache template + variables to avoid DB lookup per send
const templateCache = new Map<string, EmailTemplate & { variables: EmailTemplateVariable[] }>();
async function loadTemplate(key: string) {
if (templateCache.has(key)) {
return templateCache.get(key)!;
}
const template = await prisma.emailTemplate.findUnique({
where: { key, isActive: true },
include: { variables: true },
});
if (template) {
templateCache.set(key, template);
}
return template;
}
Best Practices
Variable Naming Conventions
Use UPPERCASE_WITH_UNDERSCORES:
// ✓ Good
USER_NAME
SHIFT_DATE
HAS_PHONE
REPRESENTATIVE_EMAIL
// ✗ Bad
userName // Not uppercase
user-name // Dashes not underscores
UserName // PascalCase
Be Descriptive:
// ✓ Good
SHIFT_START_TIME
CAMPAIGN_TITLE
IS_EMAIL_VERIFIED
// ✗ Bad
TIME // Too vague
TITLE // Ambiguous
VERIFIED // Missing context
Prefix Booleans with IS/HAS:
// ✓ Good
HAS_PHONE
IS_VERIFIED
IS_CUT_ASSIGNED
// ✗ Bad
PHONE // Not clearly boolean
VERIFIED // Ambiguous (boolean or timestamp?)
Documentation
Always Provide Labels:
// ✓ Good
label: 'User\'s Full Name',
description: 'Full name of the email recipient',
// ✗ Bad
label: 'Name', // Too generic
description: '',
Document Expected Format:
// ✓ Good
description: 'Shift date in format "Saturday, March 15, 2026"',
sampleValue: 'Saturday, March 15, 2026',
// ✗ Bad
description: 'The date',
sampleValue: '2026-03-15', // Doesn't match expected format
Sample Values
Provide Realistic Examples:
// ✓ Good
sampleValue: 'John Doe', // USER_NAME
sampleValue: 'Saturday, March 15, 2026', // SHIFT_DATE
sampleValue: '(555) 123-4567', // USER_PHONE
// ✗ Bad
sampleValue: 'test', // Not realistic
sampleValue: '123', // Not realistic phone
Use JSON for Arrays/Objects:
// ✓ Good
sampleValue: JSON.stringify([
{ name: 'Jane Doe', email: 'jane@example.com' },
{ name: 'John Smith', email: 'john@example.com' },
]),
// ✗ Bad
sampleValue: 'array of representatives', // Not parseable
Required vs Optional
Make Variables Required If:
- Used in subject line (always visible)
- Critical to email meaning (e.g., event date)
- No reasonable default value
Make Variables Optional If:
- Used in conditional blocks (
{{#if}}) - Nice-to-have but not critical
- Has fallback text in template
Related Documentation
Frontend Documentation
- EmailTemplateEditorPage.tsx — Variable insertion panel
- EmailTemplatesPage.tsx — Variables tab
Backend Documentation
- Email Templates Module — Variable CRUD API
GET /api/email-templates/:id/variables— List variablesPOST /api/email-templates/:id/variables— Create variablePUT /api/email-templates/:id/variables/:varId— Update variableDELETE /api/email-templates/:id/variables/:varId— Delete variable
Database Documentation
- Email Templates Models — EmailTemplateVariable schema
Feature Documentation
- template-system.md — Email template engine overview
- editor.md — Email template editor interface
- versioning.md — Template version history