npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mostajs/notifications

v0.1.0

Published

Generic notification engine for @mostajs/* — domain-agnostic templating + i18n + mailer dispatch + idempotence. Consume any event kind from any source (booking, media-sfu, custom apps). Plug-and-play with @mostajs/mailer.

Readme

@mostajs/notifications

Auteur : Dr Hamid MADANI [email protected] License : AGPL-3.0-or-later Version : 0.1.0

Moteur générique de notifications pour l'écosystème @mostajs/* — découple le dispatch (mailer + i18n + templating + idempotence) du domaine métier (booking, media-sfu, custom apps). Tout domaine peut envoyer des notifications en appelant engine.notify(kind, context) ; les adapters spécifiques (@mostajs/booking-notifications, @mostajs/media-sfu-notifications, …) sont de fins glues qui mappent leurs events internes en kinds génériques.

📊 Étude état de l'art (Knock, Courier, Novu, SendGrid, Postmark, Resend, Customer.io, …) → docs/STATE-OF-THE-ART.md


Table des matières

  1. Pourquoi un moteur générique
  2. Architecture
  3. Quick start — how to use
  4. API détaillée
  5. Templates — patterns recommandés
  6. Implémentation — how to impl
  7. I18n & locales
  8. Idempotence
  9. Tests
  10. Modules liés

1. Pourquoi un moteur générique

Avant ce module, chaque domaine @mostajs/* qui voulait envoyer un mail dupliquait :

  • Templates locale (FR / EN / AR)
  • Dispatch via @mostajs/mailer
  • Idempotence (messageId)
  • Retry / fallback / callbacks

Avec ce module :

  • @mostajs/booking-notifications : 100 LOC de glue (subscribe events booking → notify)
  • @mostajs/media-sfu-notifications (futur) : 80 LOC de glue (subscribe events SFU → notify)
  • @mostajs/orphan-care-notifications (futur) : idem
  • Plomberie commune (templates registry, mailer, i18n, idempotence) : un seul code à maintenir

Cas où ne PAS utiliser ce module :

  • App très simple avec 1 seul mail à envoyer → utilise @mostajs/mailer directement
  • Domaine très spécifique avec templates DB-driven (CMS-like) → roll your own avec @mostajs/mailer

2. Architecture

┌──────────────────────────────────────────────────────────────┐
│  App / Adapter consumer                                       │
│   - subscribe events @mostajs/booking, @mostajs/media-sfu, … │
│   - map event → { kind: 'booking.reservation.created',       │
│                    context: { reservation, slot, ... } }     │
│   - appelle engine.notify(...)                                │
└──────────────────────────────────┬───────────────────────────┘
                                   │
                                   ▼
┌──────────────────────────────────────────────────────────────┐
│  @mostajs/notifications  — NotificationEngine                 │
│                                                              │
│   ┌──────────────────┐   ┌──────────────────┐                │
│   │ Templates        │   │ Locale picker    │                │
│   │ registry         │ ◀─│ ctx.locale →     │                │
│   │ (kind → tpl)     │   │ variant FR/EN/AR │                │
│   └────────┬─────────┘   └──────────────────┘                │
│            ▼                                                  │
│   ┌──────────────────┐   ┌──────────────────┐                │
│   │ render(ctx)      │ → │ Mail (subject,   │                │
│   │ → RenderedMail   │   │ to, html, text)  │                │
│   └────────┬─────────┘   └────────┬─────────┘                │
│            ▼                       ▼                          │
│   ┌──────────────────────────────────────────┐               │
│   │ Idempotence (messageId)                   │               │
│   │ Callbacks (onSent / onError)              │               │
│   └────────────────────┬─────────────────────┘               │
└────────────────────────┼─────────────────────────────────────┘
                         │
                         ▼
┌──────────────────────────────────────────────────────────────┐
│  @mostajs/mailer  — driver-based dispatch                     │
│   SMTP / Resend / Postmark / SES / Brevo / Console / Mock     │
└──────────────────────────────────────────────────────────────┘

3. Quick start — how to use

Installation

npm install @mostajs/notifications @mostajs/mailer

Bootstrap minimal

import { createNotificationEngine, simpleTemplate, localizedTemplate } from '@mostajs/notifications'
import { createMailer, createConsoleDriver } from '@mostajs/mailer'

const mailer = createMailer({ driver: createConsoleDriver() })

const engine = createNotificationEngine({
  mailer,
  defaultFrom: '[email protected]',
  defaultLocale: 'fr',
  templates: {
    'welcome': simpleTemplate({
      to:      ctx => (ctx.user as any)?.email ?? null,
      subject: ctx => `Bienvenue ${(ctx.user as any)?.name}`,
      html:    ctx => `<h1>Bonjour ${(ctx.user as any)?.name}</h1><p>Heureux de vous accueillir !</p>`,
    }),
  },
})

await engine.notify('welcome', {
  user: { name: 'Amina', email: '[email protected]' },
})

Template multi-locale (FR + EN + AR)

engine.register('appointment.reminder', localizedTemplate({
  fr: {
    subject: ctx => `Rappel : votre rendez-vous ${formatDate(ctx.startAt)}`,
    html:    ctx => `<p>Bonjour, n'oubliez pas votre rendez-vous demain à <strong>${formatDate(ctx.startAt)}</strong>.</p>`,
  },
  en: {
    subject: ctx => `Reminder: your appointment ${formatDate(ctx.startAt)}`,
    html:    ctx => `<p>Hi, don't forget your appointment tomorrow at <strong>${formatDate(ctx.startAt)}</strong>.</p>`,
  },
  ar: {
    subject: ctx => `تذكير: موعدكم ${formatDate(ctx.startAt)}`,
    html:    ctx => `<p>تذكير بموعدكم غداً في <strong>${formatDate(ctx.startAt)}</strong>.</p>`,
  },
}, ctx => (ctx.user as any)?.email,
   ctx => `appt-reminder-${(ctx as any).reservationId}-24h`,  // idempotence
))

// L'app envoie selon la locale du user :
await engine.notify('appointment.reminder', {
  user: { email: '[email protected]' },
  startAt: Date.now() + 86400_000,
  reservationId: 'res-123',
  locale: 'ar',
})

Dans un adapter domain (@mostajs/booking-notifications, etc.)

// @mostajs/booking-notifications fait :
manager.onEvent = async (ev) => {
  if (ev.type !== 'reservation.created') return
  const r = await manager.findReservation(ev.reservationId!)
  const slot = await manager.findSlot(r.slotId)
  await engine.notify('booking.reservation.created', {
    reservation: r, slot, locale: r.metadata?.locale,
  })
}

4. API détaillée

createNotificationEngine(opts) → NotificationEngine

interface NotificationEngineOptions {
  mailer: { send: (mail: any) => Promise<{ messageId: string }> }
  templates?: Record<string, NotificationTemplate>
  defaultLocale?: string
  defaultFrom?: string
  disabled?: string[]
  onSent?:  (kind, context, result) => void | Promise<void>
  onError?: (kind, context, error)  => void | Promise<void>
}

interface NotificationEngine {
  register(kind: string, template: NotificationTemplate): void
  unregister(kind: string): void
  registeredKinds(): string[]
  setEnabled(kind: string, enabled: boolean): void

  notify(kind: string, context: NotificationContext): Promise<{ messageId: string } | null>
  notifyBatch(items: Array<{ kind, context }>): Promise<Array<{ messageId: string } | null>>
}

NotificationTemplate

interface NotificationTemplate {
  render(context: NotificationContext): Promise<RenderedMail | null> | RenderedMail | null
}

interface RenderedMail {
  to: string                 // requis (sinon skip)
  cc?: string | string[]
  bcc?: string | string[]
  from?: string              // override defaultFrom
  subject: string
  text?: string
  html?: string
  messageId?: string         // idempotence (mailer skip si déjà sent)
  metadata?: Record<string, unknown>
}

Helpers

  • simpleTemplate({ to, subject, text?, html?, messageId? }) — factory function-based
  • localizedTemplate(variants, resolveTo, resolveMessageId?) — multi-locale avec fallback fr → en

5. Templates — patterns recommandés

Pattern 1 — Templates inline (mini app)

templates: {
  'welcome': simpleTemplate({
    to: ctx => (ctx.user as any).email,
    subject: () => 'Bienvenue',
    html: ctx => `<h1>Hello ${(ctx.user as any).name}</h1>`,
  }),
}

Pattern 2 — Templates dans des fichiers séparés

my-app/
├── lib/
│   └── notifications.ts        ← createNotificationEngine + register
└── templates/
    ├── welcome.ts
    ├── booking-created.ts
    └── ...
// templates/welcome.ts
import { localizedTemplate } from '@mostajs/notifications'
export const welcomeTemplate = localizedTemplate({
  fr: { subject: 'Bienvenue', html: ctx => `<h1>Hello ${(ctx.user as any).name}</h1>` },
  en: { subject: 'Welcome',   html: ctx => `<h1>Hello ${(ctx.user as any).name}</h1>` },
}, ctx => (ctx.user as any).email)
// lib/notifications.ts
import { welcomeTemplate } from '../templates/welcome.js'
const engine = createNotificationEngine({ mailer, templates: { welcome: welcomeTemplate } })

Pattern 3 — Templates DB-driven (CMS)

Pour permettre à des non-devs d'éditer les templates :

const engine = createNotificationEngine({ mailer })

async function loadTemplates() {
  const rows = await db.select().from('notification_templates')
  for (const row of rows) {
    engine.register(row.kind, simpleTemplate({
      to: ctx => evalExpr(row.toExpr, ctx),
      subject: ctx => interpolate(row.subject, ctx),
      html: ctx => interpolate(row.htmlBody, ctx),
    }))
  }
}

await loadTemplates()
db.on('change:notification_templates', loadTemplates)  // hot-reload

Pattern 4 — Moteur de template externe (Handlebars, EJS)

import Handlebars from 'handlebars'

const subjectTpl = Handlebars.compile('Rappel : {{appointmentName}} le {{startAt}}')
const htmlTpl    = Handlebars.compile(htmlTemplate)

engine.register('reminder', simpleTemplate({
  to:      ctx => (ctx.user as any).email,
  subject: ctx => subjectTpl(ctx),
  html:    ctx => htmlTpl(ctx),
}))

6. Implémentation — how to impl

Adapter pour un domaine (pattern type)

// my-domain-notifications.ts
import type { NotificationEngine } from '@mostajs/notifications'
import { localizedTemplate } from '@mostajs/notifications'
import type { MyManager, MyEvent } from '@mostajs/my-domain'

export function createMyDomainNotifications(opts: {
  engine: NotificationEngine
  manager: MyManager
  /** Prefix pour les kinds : 'my-domain' → 'my-domain.event-x'. Default = nom du domain. */
  kindPrefix?: string
}) {
  const prefix = opts.kindPrefix ?? 'my-domain'

  // 1. Enregistrer les templates par défaut du domaine
  opts.engine.register(`${prefix}.welcome`, localizedTemplate({
    fr: { subject: 'Bienvenue dans mon domaine', html: ctx => `...` },
    en: { subject: 'Welcome',                    html: ctx => `...` },
  }, ctx => (ctx.entity as any)?.email))

  // 2. Subscribe aux events du manager → notify
  opts.manager.onEvent = async (ev: MyEvent) => {
    switch (ev.type) {
      case 'entity.created':
        const e = await opts.manager.findEntity(ev.entityId!)
        await opts.engine.notify(`${prefix}.welcome`, { entity: e, locale: e.locale })
        break
      // ...
    }
  }

  return {
    /** Permet à l'app d'override les templates par défaut. */
    setTemplate(kind: string, template: NotificationTemplate) {
      opts.engine.register(`${prefix}.${kind}`, template)
    },
  }
}

Routes admin (Next.js)

// app/api/admin/notifications/test/route.ts
import { engine } from '@/lib/notifications'

export async function POST(req: Request) {
  const { kind, context } = await req.json()
  const result = await engine.notify(kind, context)
  return Response.json({ sent: !!result, messageId: result?.messageId })
}

// app/api/admin/notifications/kinds/route.ts
export async function GET() {
  return Response.json({ kinds: engine.registeredKinds() })
}

7. I18n & locales

Le context.locale détermine quel template variant est choisi. Helper localizedTemplate(variants, ...) :

  • Cherche variants[locale] (ex. fr-DZ)
  • Fallback sur variants[locale.split('-')[0]] (ex. fr)
  • Fallback final sur variants.fr puis variants.en

Résolution de la locale : c'est à l'app de mettre ctx.locale (depuis user.locale DB, Accept-Language header, ou subdomain fr.app.com).

await engine.notify('reminder', {
  user: u,
  locale: u.locale ?? extractFromHeaders(req) ?? 'fr',
})

8. Idempotence

Pour éviter d'envoyer 2× le même reminder 24h si un cron retrigger :

  • Le template doit fournir un messageId déterministe : reminder-24h-${reservation.id}
  • @mostajs/mailer (Phase 2 MailLog) persistera ce messageId en DB → 2e send avec même ID = no-op silencieux
localizedTemplate(
  { fr: { subject: 'Rappel', html: ... } },
  ctx => (ctx.user as any).email,
  ctx => `reminder-24h-${(ctx.reservation as any).id}`,   // messageId
)

9. Tests

// tests/engine.test.ts
import { createNotificationEngine, simpleTemplate } from '@mostajs/notifications'

describe('NotificationEngine', () => {
  it('dispatch template registered', async () => {
    const sent: any[] = []
    const mailer = { send: async (m: any) => { sent.push(m); return { messageId: 'x-1' } } }
    const engine = createNotificationEngine({ mailer })
    engine.register('welcome', simpleTemplate({
      to:      _ => '[email protected]',
      subject: _ => 'Hello',
      text:    _ => 'Welcome!',
    }))

    const r = await engine.notify('welcome', { user: 'x' })
    expect(r?.messageId).toBe('x-1')
    expect(sent[0]).toMatchObject({ to: '[email protected]', subject: 'Hello' })
  })

  it('skip if template absent', async () => {
    const engine = createNotificationEngine({ mailer: { send: async () => ({ messageId: '' }) } })
    expect(await engine.notify('unknown', {})).toBeNull()
  })

  it('locale fallback', async () => {
    // ... test localizedTemplate avec variant manquante
  })
})

10. Modules liés

  • @mostajs/mailer — peer dep, dispatch SMTP/Resend/SES/etc.
  • @mostajs/booking-notifications — adapter booking events → notifications
  • (futur) @mostajs/media-sfu-notifications — adapter SFU events
  • @mostajs/url — pour construire des liens dans les templates (signedUrl pour invite tokens)

License : AGPL-3.0-or-later Auteur : Dr Hamid MADANI [email protected]