elon-email-templates
Elon AI Email Templates
Overview
HTML string-based email templates for the Elon AI Classroom Assistant platform. This project does NOT use React Email components - all templates are pure HTML strings with TypeScript helper functions.
Important: The global
react-emailskill documents React Email patterns. This project uses a different approach optimized for simplicity and performance. Follow this skill for Elon AI email work.
When to Use
- Creating new transactional email templates
- Modifying existing templates in
email-send.ts - Understanding the email queue architecture
- Adding new email types to the system
Architecture
Email Flow
API/Action → publishJob({jobType: "email:send"})
→ Upstash QStash (queue)
→ emailSendHandler()
→ Template selection (EMAIL_TEMPLATES[templateId])
→ HTML string generation
→ Resend API
→ User inbox
Key Files
| File | Purpose |
|---|---|
lib/jobs/handlers/email-send.ts |
All 12 email templates + handler (916 lines) |
lib/email/constants.ts |
Centralized colors, styles, logo URL |
lib/email/unsubscribe-token.ts |
HMAC-signed unsubscribe tokens |
app/api/email/unsubscribe/route.ts |
Unsubscribe API endpoint |
lib/jobs/types.ts |
EmailSendPayloadSchema definition |
.email-previews/ |
HTML preview files for visual testing |
Template Pattern
All templates use pure HTML strings with helper functions:
// Template structure in EMAIL_TEMPLATES object
welcome: (data) => ({
subject: "Welcome to Elon AI!",
html: wrapInBaseTemplate({
headerTitle: `Welcome, ${escapeHtml(data.name)}!`,
headerSubtitle: "Your AI-powered study companion is ready",
content: `
<p style="${STYLES.bodyText}">
Hey ${escapeHtml(data.name)}, welcome to Elon AI!
</p>
${createButton("Get Started", url)}
`,
proTip: "Your conversations are FERPA-protected.",
}),
}),
Helper Functions
| Function | Purpose | Example |
|---|---|---|
wrapInBaseTemplate() |
Header + content + footer wrapper | See above |
createButton() |
Primary/secondary CTA buttons | createButton("Click Me", url, "primary") |
createInfoBox() |
Key-value info cards | createInfoBox([{label: "Name", value: "John"}]) |
escapeHtml() |
XSS protection for user content | escapeHtml(user.name) |
Style Constants
All styles are defined in STYLES object at top of email-send.ts:
const STYLES = {
container: "...", // Max-width 600px centered layout
header: "...", // Maroon gradient header
headerTitle: "...", // White text for header
body: "...", // White background body
bodyText: "...", // Gray text paragraphs
button: "...", // Maroon primary button
buttonSecondary: "...", // White bordered button
infoBox: "...", // Gray background info card
proTipBox: "...", // Gold accent tip box
footer: "...", // Gray footer
// ... more
};
Existing Templates (12)
| Template ID | Purpose | Key Data Fields |
|---|---|---|
welcome |
New student onboarding | name |
role_change |
User role changed | userName, oldRole, newRole |
assistant_published |
Teacher's assistant goes live | assistantName, courseName, joinCode |
budget_warning |
Admin usage alert | percentUsed, currentSpend, budgetLimit |
processing_failed |
File upload error | fileName, errorMessage |
new_student_joined |
Admin notification | studentName, studentEmail, joinedAt |
role_request |
Admin action needed | userName, userEmail, requestedRole |
role_approved |
Role request approved | name, newRole, tips[], dashboardUrl |
role_denied |
Role request denied | name, requestedRole, reason |
teacher_digest |
Weekly analytics | totalSessions, uniqueStudents, satisfactionRate, topTopic |
qr_join_code |
QR code invitation | assistantName, courseName, joinCode, qrCodeUrl |
Adding a New Template
Step 1: Add to EMAIL_TEMPLATES object
// In lib/jobs/handlers/email-send.ts
const EMAIL_TEMPLATES = {
// ... existing templates
// Your new template
my_new_template: (data) => ({
subject: `Your subject with ${escapeHtml(data.dynamicValue)}`,
html: wrapInBaseTemplate({
headerTitle: "Header Title",
headerSubtitle: "Optional subtitle",
content: `
<p style="${STYLES.bodyText}">
${escapeHtml(data.message)}
</p>
${createButton("Action", data.actionUrl)}
`,
proTip: "Optional helpful tip",
}),
}),
};
Step 2: Add payload schema (if needed)
If your template needs specific data validation, update lib/jobs/types.ts:
export const EmailSendPayloadSchema = z.object({
tenantId: z.string().uuid(),
templateId: z.string(), // Template ID
to: z.string().email(),
subject: z.string().optional(), // Override template subject
data: z.record(z.unknown()), // Template-specific data
});
Step 3: Trigger the email
import { publishJob } from "@/lib/jobs/publisher";
await publishJob({
jobType: "email:send",
payload: {
tenantId: user.tenantId,
templateId: "my_new_template",
to: user.email,
data: {
message: "Hello world!",
actionUrl: "https://elon-ai.app/action",
},
},
});
Step 4: Generate preview
Add test data and regenerate previews:
pnpm email:preview
Security Requirements
XSS Protection
ALWAYS use escapeHtml() on user-controlled content:
// CORRECT - escaped
<p>${escapeHtml(data.userName)}</p>
// WRONG - XSS vulnerability!
<p>${data.userName}</p>
FERPA Compliance
- Include
tenantIdin all email payloads - All sends are logged to
audit_logstable - Don't include student PII in email subjects
- Unsubscribe tokens use HMAC-SHA256 signing
Multi-Tenant Scoping
- Emails are always scoped to a tenant
- Job queue handles tenant isolation
- Audit logs record tenant context
Email Client Compatibility
DO
- Use PNG/JPG images with absolute URLs
- Use inline CSS styles (no classes)
- Use table-based layouts for columns
- Keep emails under 102KB
- Test in Gmail, Outlook, Apple Mail
DON'T
- Use SVG images (Gmail strips them)
- Use CSS Grid or Flexbox
- Use media queries (limited support)
- Use external CSS files
- Use JavaScript
Logo
The logo is a hosted PNG at public/email-assets/elon-ai-logo.png:
// Defined in lib/jobs/handlers/email-send.ts
const EMAIL_LOGO_URL = "https://elon-ai.app/email-assets/elon-ai-logo.png";
const LOGO_IMG = `<img src="${EMAIL_LOGO_URL}" alt="Elon AI" width="48" height="48" />`;
To regenerate the logo PNG:
pnpm tsx scripts/generate-email-logo.ts
Testing
Visual Previews
Open .email-previews/index.html to browse all templates visually.
Unit Tests
pnpm test tests/unit/email/
Integration Tests
pnpm test tests/integration/api/unsubscribe.test.ts
Development Mode
When RESEND_API_KEY is not set, emails are logged to console instead of sent.
Brand Colors
| Color | Hex | Usage |
|---|---|---|
| Maroon | #73000a |
Headers, buttons, primary text |
| Gold | #b59a57 |
Accents, pro tips, highlights |
| White | #ffffff |
Body backgrounds, button text |
| Gray-50 | #f9fafb |
Footer, info box backgrounds |
| Gray-700 | #374151 |
Body text |
Common Patterns
Teacher Analytics Email
The teacher_digest template demonstrates advanced patterns:
- Dynamic headlines based on metrics
- Conditional content based on activity level
- Unsubscribe link with HMAC token
- Color-coded metric changes
QR Code Email
The qr_join_code template shows how to include images:
- Hosted QR code image URL
- Fallback text if image fails
- Clear call-to-action