Message Templates
Scriban-powered bilingual message templates with preview, built-in templates, and placeholder schemas.
Template Architecture
The Message Templates system provides a centralized way to manage email, notification, and webhook message content. Templates use the Scriban engine (Liquid-like syntax) for variable substitution, conditionals, and loops.
Scriban Template Syntax
<!-- Variable substitution -->
Hello {{ admin.name }},
<!-- Conditional content -->
{{ if admin.is_protected }}
You are the super admin for {{ tenant.name }}.
{{ end }}
<!-- Loops -->
{{ for role in admin.roles }}
- {{ role.name }}
{{ end }}
<!-- Filters (pipes) -->
Created: {{ created_at | date.to_string "%B %d, %Y" }}
Amount: {{ amount | math.format "0.00" }}Built-in Templates
| Template Key | Trigger | Variables |
|---|---|---|
| welcome_admin | Admin creation | admin.name, admin.email, tenant.name, login_url |
| password_reset | Forgot password | admin.name, reset_url, expiry_minutes |
| otp_code | Two-factor auth | admin.name, otp_code, expiry_minutes |
| admin_blocked | Account blocked | admin.name, reason, support_email |
| admin_unblocked | Account unblocked | admin.name, login_url |
| email_verification | Email verification | admin.name, verification_url, expiry_hours |
MessageTemplate Entity
public class MessageTemplate : AuditableEntity<Guid>
{
[Required] [MaxLength(100)]
public string Key { get; set; } = null!; // Unique key, e.g., "welcome_admin"
public MessageChannel Channel { get; set; } // Email, SMS, Push
[MaxLength(500)]
public string? Subject { get; set; } // Subject with variables
[Required] [MaxLength(50000)]
public string Body { get; set; } = null!; // HTML/Text body with Scriban template syntax
[Required] [MaxLength(10)]
public string Language { get; set; } = "en"; // "en" or "ar"
public new bool IsActive { get; set; } = true;
public Guid? TenantId { get; set; } // Null = system-wide, set = tenant override
[MaxLength(500)]
public string? Description { get; set; }
[MaxLength(4000)]
public string? PlaceholderSchema { get; set; } // JSON array of placehoders (key, type, required)
[MaxLength(2000)]
public string? DesignVariables { get; set; } // Theme-able CSS variables in JSON format
public int Version { get; set; } = 1; // Auto-incremented version number
[MaxLength(50)]
public string? Category { get; set; } // e.g. Transactional, Marketing
[MaxLength(500)]
public string? Tags { get; set; }
}Template Renderer
public class TemplateRenderer : ITemplateRenderer
{
private static string RenderWithScriban(string templateText, Dictionary<string, object> data)
{
var parsedTemplate = Template.Parse(templateText);
if (parsedTemplate.HasErrors)
{
// Syntax error fallback: Safe literal double curly braces replacement
var result = templateText;
foreach (var kvp in data)
{
result = result.Replace($"{{{{{kvp.Key}}}}}", kvp.Value?.ToString() ?? "");
}
return result;
}
var scriptObject = new ScriptObject();
foreach (var kvp in data)
{
scriptObject[kvp.Key] = kvp.Value;
}
var context = new TemplateContext();
context.PushGlobal(scriptObject);
return parsedTemplate.Render(context);
}
}Placeholder Schemas & Schema Validation
Every template declares a structured JSON schema of allowed placeholders, validating merge variables before execution to eliminate runtime rendering crashes.
// Validation rule enforcing correct JSON syntax on placeholder schema & design variables
RuleFor(x => x.PlaceholderSchema)
.MaximumLength(4000)
.Must(BeValidJson)
.When(x => !string.IsNullOrWhiteSpace(x.PlaceholderSchema))
.WithMessage("PlaceholderSchema must be valid JSON.");
private static bool BeValidJson(string? json)
{
if (string.IsNullOrWhiteSpace(json)) return true;
try
{
System.Text.Json.JsonDocument.Parse(json);
return true;
}
catch { return false; }
}Template Versioning & Rollback
Templates track full modification history, author attribution, and revision notes with instant one-click rollback to any previous version.
// Validate Scriban syntax and increment template version on change
var parsed = Template.Parse(request.Body);
if (parsed.HasErrors)
return Error.Validation("Template body has syntax errors.");
template.Subject = request.Subject;
template.Body = request.Body;
template.PlaceholderSchema = request.PlaceholderSchema;
template.Version += 1; // Auto-increment version
await _unitOfWork.SaveChangesAsync(cancellationToken);Template API Endpoints
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| GET | /message-templates | List all templates | 🔒 Required |
| GET | /message-templates/{id} | Get template detail | 🔒 Required |
| POST | /message-templates | Create custom template | 🔒 Required |
| PUT | /message-templates/{id} | Update template | 🔒 Required |
| DELETE | /message-templates/{id} | Delete (non-system only) | 🔒 Required |
| POST | /message-templates/preview | Render with sample data | 🔒 Required |
Preview Feature
The preview endpoint renders a template with sample data, allowing admins to see exactly how the email or notification will look before sending. This is invaluable for testing templates and catching formatting issues.
// POST /api/v1/message-templates/preview
{
"subject": "Welcome {{ admin.name }}",
"body": "<p>Hello {{ admin.name }}, welcome to {{ tenant.name }}!</p>",
"sampleData": {
"admin": { "name": "John Doe", "email": "john@example.com" },
"tenant": { "name": "ACME Corp" }
}
}
// Response:
// {
// "subject": "Welcome John Doe",
// "body": "<p>Hello John Doe, welcome to ACME Corp!</p>"
// }