Skip to content

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)

ColumnTypeDescription
idintegerAlways 1
email_transportvarcharsmtp, ses, sendmail
smtp_hostvarcharSMTP server hostname
smtp_portintegerSMTP port
smtp_uservarcharSMTP username
smtp_passvarcharSMTP password
smtp_securebooleanUse TLS
ses_regionvarcharAWS SES region
ses_access_keyvarcharAWS access key
ses_secret_keyvarcharAWS secret key
email_fromvarcharSender email address
admin_emailsjsonAdmin email list for cc_admin
mattermost_urlvarcharMattermost server URL
mattermost_tokenvarcharMattermost bot token
default_localevarcharDefault language (e.g., en)
default_channelsjsonDefault channels for all sends

odp_notify_layouts

ColumnTypeDescription
idvarcharLayout ID (e.g., default, minimal)
namevarcharDisplay name
descriptionvarcharDescription
contenttextHTML with {{{content}}} placeholder
variables_schemajsonJSON schema for layout variables

odp_notify_templates

ColumnTypeDescription
idvarcharTemplate ID (e.g., welcome_email)
namevarcharDisplay name
channelsjsonArray of channel IDs
cc_adminbooleanCC admin emails on send
variables_schemajsonJSON schema for template variables
layout_idvarcharFK to odp_notify_layouts.id
statusvarcharpublished, draft, archived

odp_notify_template_contents

ColumnTypeDescription
idintegerPrimary key
template_idvarcharFK to odp_notify_templates.id
providervarcharProvider ID (e.g., email, inapp)
localevarcharLanguage code (e.g., en, vi)
subjectvarcharSubject line (for email)
contenttextContent body (Handlebars template)
metadatajsonProvider-specific metadata
activebooleanIs this content active

odp_notify_logs

ColumnTypeDescription
idintegerPrimary key
statusvarcharpending, sending, sent, failed, cancelled
template_idvarcharFK to template (nullable for direct sends)
channelvarcharProvider channel used
recipientsjsonArray of recipient addresses
ccjsonCC recipients
bccjsonBCC recipients
localevarcharLanguage used
variablesjsonVariables passed at trigger time
subject_renderedvarcharFinal rendered subject
content_renderedtextFinal rendered content
scheduled_attimestampWhen to send (null = immediate)
sent_attimestampActual send time
error_messagetextError description on failure
provider_responsejsonRaw 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"
}
FieldRequiredDescription
template_idYesTemplate to use
recipientsYesArray of recipient addresses (email/user IDs)
ccNoCC recipients
bccNoBCC recipients
variablesNoTemplate variable values
localeNoLanguage code (defaults to default_locale)
channelsNoOverride template's default channels
scheduled_atNoISO 8601 timestamp for delayed send
metadataNoArbitrary 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:

handlebars
Hello {{user_name}},

Your invitation link: {{{invite_url}}}

{{#if is_admin}}
You have admin access.
{{/if}}
  • {{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() in server.ts
  • Scheduled sends are processed when scheduled_at <= NOW
  • Failed sends are logged with error_message set
  • Workers respect FILES_MAX_UPLOAD_CONCURRENCY (reuses the same concurrency limit)

Environment Variables

VariableDefaultDescription
EMAIL_FROMno-reply@example.comDefault sender address
EMAIL_TRANSPORTsmtpTransport: smtp, ses, sendmail
EMAIL_SMTP_HOSTlocalhostSMTP host
EMAIL_SMTP_PORT587SMTP port
EMAIL_SMTP_USER``SMTP username
EMAIL_SMTP_PASSWORD``SMTP password
EMAIL_SMTP_SECUREfalseUse TLS

ODP Internal API Documentation