@nage-api/notify
v1.0.0-beta.4
Published
Notifications for @nage-api — channel abstraction and templates
Readme
@nage-api/notify
Channel-abstracted notifications with templates (PLAN.md §8, §25 P2).
The application says "tell this user their order shipped"; whether that becomes an email, an SMS, a push, or all three is configuration and per-recipient preference, not a branch in domain code.
import { NotifyService } from '@nage-api/notify';
// Exported by `NageNotifyModule.forRoot`, so injecting the class is enough.
declare const notify: NotifyService;
declare const user: { id: string; email: string; name: string };
declare const order: { reference: string };
const report = await notify.send({
template: 'order.shipped',
to: { id: user.id, email: user.email, locale: 'fr-CA' },
data: { user, order },
});
// → { template: 'order.shipped', results: [ … ], delivered: true, skipped: ['sms'] }Three properties, in order of importance
Preferences are consulted first. A recipient who opted out of marketing SMS must not get one because a new code path forgot to check, so the check lives in the service rather than at every call site. Opt-outs can be per channel or per channel-and-template.
One channel failing does not fail the rest. A dead SMS provider should not stop the email, so channels are attempted independently and every outcome is reported.
A failure never carries the body. A notification body holds whatever the template put in it — a name, an address, an order — and error paths end up in logs that live far longer than the message did.
Templates have no expression language
{{ user.name }} interpolates a value. That is the entire grammar: no {{#if}},
no method calls, no property access that reaches a prototype. The moment a
template can evaluate an expression, a template stored in a database becomes
remote code execution — and "let marketing edit the emails" is a feature every
product eventually ships. {{ constructor.name }} and {{ __proto__.x }}
render as empty strings.
Anything the grammar cannot express belongs in the code that builds the data.
Locale selection falls back exact → language → default, and interpolated
values are HTML-escaped in the html body only.
Channels
MemoryChannel records instead of sending, so a developer can exercise a
signup flow without an SMTP account and a test can assert on the message that
would have gone out. Like the queue's memory driver, it is refused in
production — it would silently swallow every message.
Not yet implemented
- Real channels.
@nage-api/integrations-emailand@nage-api/integrations-smsare where they belong;NotificationChannelis the port they implement. - Template loading from
notify.templateDirectory; templates are registered in code today. notify.from. It is declared inNotifyConfigand this package never reads it, so the sender address belongs to whichever channel you supply.- Retries.
sendreports a channel's failure and returns; nothing re-attempts it. Retrying is@nage-api/queue's job, from a job that callssend. - Digest/batching, and delivery-receipt tracking.
