Skip to Content
Getting started

Getting started

Prerequisites

  • Node.js 24+
  • Docker (for the example consumer image), or local Node for the API in watch mode

From the repo root:

git clone https://github.com/blockqueue/notifier.git cd notifier docker compose up --build bq-example-notifier-service

That 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.js

Samples 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/send

Template 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/send

Example: 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/send

Example: 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/send

Images 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.

Next

Last updated on