Appearance
Notification Module
Overview
The Notification Module (src/modules/notify/) provides a template-based, multi-channel notification system. It supports:
- Email (SMTP, AWS SES, Sendmail)
- In-app notifications
- Mattermost (webhook)
Notifications are defined as templates with per-provider, per-locale contents. A layout wraps email content in a shared HTML shell. Logs track every send attempt.
Architecture
Data Model
odp_notify_settings (singleton)
| Column | Type | Description |
|---|---|---|
id | integer | Always 1 |
email_transport | varchar | smtp, ses, sendmail |
smtp_host | varchar | SMTP server hostname |
smtp_port | integer | SMTP port |
smtp_user | varchar | SMTP username |
smtp_pass | varchar | SMTP password |
smtp_secure | boolean | Use TLS |
ses_region | varchar | AWS SES region |
ses_access_key | varchar | AWS access key |
ses_secret_key | varchar | AWS secret key |
email_from | varchar | Sender email address |
admin_emails | json | Admin email list for cc_admin |
mattermost_url | varchar | Mattermost server URL |
mattermost_token | varchar | Mattermost bot token |
default_locale | varchar | Default language (e.g., en) |
default_channels | json | Default channels for all sends |
odp_notify_layouts
| Column | Type | Description |
|---|---|---|
id | varchar | Layout ID (e.g., default, minimal) |
name | varchar | Display name |
description | varchar | Description |
content | text | HTML with {{{content}}} placeholder |
variables_schema | json | JSON schema for layout variables |
odp_notify_templates
| Column | Type | Description |
|---|---|---|
id | varchar | Template ID (e.g., welcome_email) |
name | varchar | Display name |
channels | json | Array of channel IDs |
cc_admin | boolean | CC admin emails on send |
variables_schema | json | JSON schema for template variables |
layout_id | varchar | FK to odp_notify_layouts.id |
status | varchar | published, draft, archived |
odp_notify_template_contents
| Column | Type | Description |
|---|---|---|
id | integer | Primary key |
template_id | varchar | FK to odp_notify_templates.id |
provider | varchar | Provider ID (e.g., email, inapp) |
locale | varchar | Language code (e.g., en, vi) |
subject | varchar | Subject line (for email) |
content | text | Content body (Handlebars template) |
metadata | json | Provider-specific metadata |
active | boolean | Is this content active |
odp_notify_logs
| Column | Type | Description |
|---|---|---|
id | integer | Primary key |
status | varchar | pending, sending, sent, failed, cancelled |
template_id | varchar | FK to template (nullable for direct sends) |
channel | varchar | Provider channel used |
recipients | json | Array of recipient addresses |
cc | json | CC recipients |
bcc | json | BCC recipients |
locale | varchar | Language used |
variables | json | Variables passed at trigger time |
subject_rendered | varchar | Final rendered subject |
content_rendered | text | Final rendered content |
scheduled_at | timestamp | When to send (null = immediate) |
sent_at | timestamp | Actual send time |
error_message | text | Error description on failure |
provider_response | json | Raw provider API response |
Settings Endpoints
GET /notify/settings
Get notification settings (singleton).
Auth required: Yes
Response 200
json
{
"data": {
"id": 1,
"email_transport": "smtp",
"smtp_host": "smtp.example.com",
"smtp_port": 587,
"email_from": "no-reply@example.com",
"default_locale": "en",
"default_channels": ["email"]
}
}PATCH /notify/settings
Update notification settings.
Auth required: Yes
Request Body
json
{
"email_transport": "smtp",
"smtp_host": "smtp.example.com",
"smtp_port": 587,
"smtp_user": "user@example.com",
"smtp_pass": "password",
"smtp_secure": true,
"email_from": "no-reply@example.com"
}Provider Endpoints
GET /notify/providers
List all registered notification providers with their status.
Auth required: Yes
Response 200
json
{
"data": [
{
"id": "email",
"name": "Email",
"description": "Send via SMTP/SES",
"icon": "email"
},
{
"id": "inapp",
"name": "In-App",
"description": "In-application notifications",
"icon": "notifications"
},
{
"id": "mattermost",
"name": "Mattermost",
"description": "Send to Mattermost channels",
"icon": "chat"
}
]
}Layout Endpoints
GET /notify/layouts
List all layouts.
POST /notify/layouts
Create a layout.
Request Body
json
{
"id": "branded",
"name": "Branded Layout",
"description": "ODP branded email template",
"content": "<html>...<div style=\"max-width:600px\">{{{content}}}</div>...</html>"
}The content field must contain the {{{content}}} placeholder (triple braces = unescaped HTML).
GET /notify/layouts/:id
PATCH /notify/layouts/:id
DELETE /notify/layouts/:id
Template Endpoints
GET /notify/templates
List all templates.
POST /notify/templates
Create a template.
Request Body
json
{
"id": "welcome_email",
"name": "Welcome Email",
"channels": ["email"],
"cc_admin": false,
"layout_id": "default",
"status": "published",
"variables_schema": {
"type": "object",
"properties": {
"user_name": { "type": "string" },
"login_url": { "type": "string" }
}
}
}GET /notify/templates/:id
Read a template (includes contents array).
Response 200
json
{
"data": {
"id": "welcome_email",
"name": "Welcome Email",
"channels": ["email"],
"status": "published",
"contents": [
{
"id": 1,
"provider": "email",
"locale": "en",
"subject": "Welcome to ODP, {{user_name}}!",
"content": "<p>Hello {{user_name}},</p><p>Click <a href=\"{{login_url}}\">here</a> to log in.</p>",
"active": true
}
]
}
}PATCH /notify/templates/:id
DELETE /notify/templates/:id
System templates (created by the system) cannot be deleted — only their status can be changed to archived.
Template Content Endpoints
GET /notify/templates/:templateId/contents
List all content variants for a template.
POST /notify/templates/:templateId/contents
Add a new content variant.
Request Body
json
{
"provider": "email",
"locale": "vi",
"subject": "Chào mừng {{user_name}} đến với ODP!",
"content": "<p>Xin chào {{user_name}},</p>",
"active": true
}PATCH /notify/contents/:id
Update a content variant.
DELETE /notify/contents/:id
Delete a content variant.
Log Endpoints
GET /notify/logs
List notification delivery logs.
Supports standard query system.
GET /notify/logs/:id
Read a single log entry.
PATCH /notify/logs/:id/cancel
Cancel a pending notification (status must be pending).
Response 200 — Returns the log ID.
DELETE /notify/logs/:id
Delete a log entry.
GET /notify/logs/stats
Get aggregate statistics.
Response 200
json
{
"data": {
"total": 500,
"sent": 480,
"failed": 10,
"pending": 5,
"cancelled": 5
}
}Trigger Endpoint
POST /notify/trigger
Trigger a notification by template ID.
Auth required: Yes
Request Body
json
{
"template_id": "welcome_email",
"recipients": ["user@example.com"],
"cc": ["manager@example.com"],
"variables": {
"user_name": "John Doe",
"login_url": "https://app.example.com/login"
},
"locale": "en",
"channels": ["email"],
"scheduled_at": "2026-03-26T12:00:00.000Z"
}| Field | Required | Description |
|---|---|---|
template_id | Yes | Template to use |
recipients | Yes | Array of recipient addresses (email/user IDs) |
cc | No | CC recipients |
bcc | No | BCC recipients |
variables | No | Template variable values |
locale | No | Language code (defaults to default_locale) |
channels | No | Override template's default channels |
scheduled_at | No | ISO 8601 timestamp for delayed send |
metadata | No | Arbitrary metadata stored in the log |
Response 200
json
{
"data": {
"log_ids": [42, 43]
}
}One log entry is created per channel per send.
Test Send Endpoint
POST /notify/test-send
Send a test notification directly via a provider (bypasses templates).
Auth required: Yes
Request Body
json
{
"provider": "email",
"recipients": ["test@example.com"],
"subject": "Test Notification",
"content": "<p>This is a test.</p>",
"metadata": {}
}Response 200
json
{
"data": {
"success": true,
"message": "Email sent successfully"
}
}Template Validation Endpoint
POST /notify/validate-template
Validate a Handlebars template string for syntax errors.
Request Body
json
{
"template": "Hello {{user_name}}, your code is {{code}}."
}Response 200
json
{
"data": {
"valid": true,
"errors": []
}
}Template Rendering
Templates use Handlebars syntax:
{{variable}}— HTML-escaped output{{{variable}}}— Raw unescaped HTML (use for URLs and HTML content){{#if condition}}...{{/if}}— Conditional blocks{{#each list}}...{{/each}}— Loop blocks
Layouts wrap template content using {{{content}}} (unescaped triple brace).
Worker & Async Processing
The NotifyWorker processes pending log entries asynchronously:
- Runs on the same server process (not a separate process)
- Registered via
registerNotifyWorker()inserver.ts - Scheduled sends are processed when
scheduled_at <= NOW - Failed sends are logged with
error_messageset - Workers respect
FILES_MAX_UPLOAD_CONCURRENCY(reuses the same concurrency limit)
Environment Variables
| Variable | Default | Description |
|---|---|---|
EMAIL_FROM | no-reply@example.com | Default sender address |
EMAIL_TRANSPORT | smtp | Transport: smtp, ses, sendmail |
EMAIL_SMTP_HOST | localhost | SMTP host |
EMAIL_SMTP_PORT | 587 | SMTP port |
EMAIL_SMTP_USER | `` | SMTP username |
EMAIL_SMTP_PASSWORD | `` | SMTP password |
EMAIL_SMTP_SECURE | false | Use TLS |