Getting started
Prerequisites
- Node.js 24+
- Docker (for the example consumer image), or local Node for the API in watch mode
Example service (recommended first run)
From the repo root:
git clone https://github.com/blockqueue/notifier.git
cd notifier
docker compose up --build bq-example-notifier-serviceThat builds the base notifier image and the sample consumer under examples/notifier-service, then listens on port 3000.
For Docker, use a credentials-only env file. Do not set CONFIG_PATH or TEMPLATES_DIR on a baked image (defaults are /config/config.yaml and /app/templates). The monorepo .env.example points at source paths for local API only.
Local API (source templates)
npm install
cp apps/notifier/.env.example apps/notifier/.env
npm run dev --workspace=notifier.env.example points CONFIG_PATH and TEMPLATES_DIR at examples/notifier-service. Fill in real provider credentials when you want real sends.
Preview templates (not the API)
cd examples/notifier-service
npm install
npm run dev- React Email preview: port 10001
- MJML/HTML preview: port 10002 (localhost)
Smoke send (HMAC)
The example config uses HMAC. With the API up and NOTIFIER_SIGNING_SECRET=local-signing-secret:
NOTIFIER_SIGNING_SECRET=local-signing-secret \
node examples/notifier-service/scripts/send-welcome-mjml.js
# Try other sample templates (welcome, OTP, order receipt)
NOTIFIER_SIGNING_SECRET=local-signing-secret \
node examples/notifier-service/scripts/send-welcome-mjml.js --sample=3
NOTIFIER_SIGNING_SECRET=local-signing-secret \
node examples/notifier-service/scripts/send-sms.jsSamples 1 through 7 cover MJML/HTML/React Email welcome, login OTP, and order receipts with line items. See the script’s --help for the map.
Minimal send from your app
import crypto from 'node:crypto';
const secret = process.env.NOTIFIER_SIGNING_SECRET;
const body = JSON.stringify({
templateId: 'mjml-user-welcome',
payload: { userName: 'Ada', appName: 'MyApp' },
sendMailOptions: {
to: ['[email protected]'],
subject: 'Welcome',
},
});
const t = Math.floor(Date.now() / 1000);
const v1 = crypto
.createHmac('sha512', secret)
.update(`${t}.`)
.update(body)
.digest('hex');
const res = await fetch('http://127.0.0.1:3000/email/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-notifier-signature': `t=${t},v1=${v1}`,
},
body,
});
console.log(res.status, await res.json());API key auth is the same route with x-notifier-api-key instead of a signature. See Configuration and HTTP API.
Consumer layout
Canonical layout: examples/notifier-service (config, templates, Dockerfile, send scripts). Copy that pattern for each environment or product that owns its own templates. The example nests templates under project-a / project-b to show several products sharing one consumer image; every id still has to be unique.
Example: welcome email from your app
Your API creates a user. It does not import React Email or the Zeptomail SDK. It only calls notifier:
const body = JSON.stringify({
templateId: 'react-email-user-welcome',
payload: { userName: user.name, appName: 'FirmLyt', ctaUrl: inviteUrl },
sendMailOptions: { to: [user.email], subject: 'Welcome' },
});
// sign or API-key header, POST /email/sendTemplate HTML and provider credentials stay in the notifier consumer image. Shipping a new welcome layout is a notifier rebuild, not a change in every product app.
Example: login OTP (email)
Auth service issues a code. Same HTTP call, different templateId:
const body = JSON.stringify({
templateId: 'mjml-login-otp',
payload: {
userName: user.name,
appName: 'FirmLyt',
otpCode: code,
expiresInMinutes: 10,
},
sendMailOptions: { to: [user.email], subject: 'Your login code' },
});
// sign or API-key header, POST /email/sendExample: order receipt
Checkout finishes. Line items live in the payload; the template loops over them (Handlebars #each or React map):
const body = JSON.stringify({
templateId: 'mjml-order-receipt',
payload: {
orderId: order.id,
customerName: order.customerName,
appName: 'FirmLyt',
lineItems: order.items.map((i) => ({
name: i.name,
quantity: i.qty,
price: i.priceLabel,
})),
total: order.totalLabel,
trackingUrl: order.trackingUrl,
},
sendMailOptions: {
to: [order.email],
subject: `Order ${order.id} confirmed`,
},
});
// sign or API-key header, POST /email/sendExample: SMS from your app
SMS has no templates. Your app owns the body text:
const body = JSON.stringify({
to: user.phoneE164,
body: `Your FirmLyt code is ${code}. Expires in 10 minutes.`,
sendOptions: { version: 'v4', channel: 'dnd', messageType: 'plain' },
});
// sign or API-key header, POST /sms/sendImages and deploy
Base runtime (tag releases):
docker pull ghcr.io/blockqueue/notifier:<tag>Your consumer image should FROM that base, compile/copy templates, and copy config.yaml (see examples/notifier-service/Dockerfile and Templates).
In-repo examples:
- Root
docker-compose.yml: builds and runs the example consumer examples/notifier-service: config, templates, Dockerfile, send scripts
Run on any orchestrator you use (Compose, Swarm, Kubernetes, and so on). Typical layout: consumer image on an internal network, credentials in env (not in git), send routes not on the public internet, product apps calling an internal hostname. Multiple replicas of the same consumer image are fine for availability; they share the same baked templates and config.