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.

Template Architecture
MessageTemplatesController
EmailService
NotificationService
WebhookService
CRUD Operations
Preview Rendering
ITemplateRenderer
Scriban Engine (Liquid-like)

Scriban Template Syntax

Scriban Template SyntaxHTML
<!-- 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 KeyTriggerVariables
welcome_adminAdmin creationadmin.name, admin.email, tenant.name, login_url
password_resetForgot passwordadmin.name, reset_url, expiry_minutes
otp_codeTwo-factor authadmin.name, otp_code, expiry_minutes
admin_blockedAccount blockedadmin.name, reason, support_email
admin_unblockedAccount unblockedadmin.name, login_url
email_verificationEmail verificationadmin.name, verification_url, expiry_hours

MessageTemplate Entity

MessageTemplate.csC#
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

TemplateRenderer.csC#
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.

UpdateMessageTemplateCommandValidator.csC#
// 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.

UpdateMessageTemplateCommandHandler.csC#
// 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

MethodEndpointDescriptionAuth
GET/message-templatesList all templates🔒 Required
GET/message-templates/{id}Get template detail🔒 Required
POST/message-templatesCreate custom template🔒 Required
PUT/message-templates/{id}Update template🔒 Required
DELETE/message-templates/{id}Delete (non-system only)🔒 Required
POST/message-templates/previewRender 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.

Preview RequestJSON
// 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>"
// }