@growth-labs/notify
v0.2.0
Published
Worker-compatible human notification delivery for Slack and email with severity routing, retry, and caller-supplied dedup hooks.
Readme
@growth-labs/notify
Worker-compatible human notification delivery for Slack and email.
Use this package when a Worker needs to alert people about operational events: daily digest completion/failure, auth monitor incidents, Tail Worker error capture, or migration status updates.
It is deliberately small:
- Slack incoming webhooks
- Cloudflare Send Email binding
- Resend REST API fallback
- fixed severity routing
- caller-supplied dedup and audit hooks
- no D1, KV, queues, or background work
Install
pnpm add @growth-labs/notifyBasic Usage
import { notify } from '@growth-labs/notify'
const result = await notify(env, {
channels: ['slack', 'email'],
severity: 'critical',
title: 'Auth surface down',
body: '/login/ returning 503 for 12 minutes. Last good: 09:32 UTC.',
dedupKey: 'auth-monitor:fronts.co:login:down',
dedupFn: async (key) => {
// Caller-owned persistence, for example D1 or KV.
return false
},
onSent: async (record) => {
// Caller-owned audit log.
console.log(record)
},
})
if (result.failed.length > 0) {
console.warn('Notification delivery failed', result.failed)
}Severity Routing
The caller always provides the requested channel list. Severity filters that list:
| Severity | Slack | Email |
| --- | --- | --- |
| info | yes | no |
| warning | yes | no |
| critical | yes | yes |
Example: channels: ['email'] with severity: 'info' sends nothing and
returns:
{
attempted: ['email'],
sent: [],
skipped: [{ channel: 'email', reason: 'severity-routing' }],
failed: [],
}Routing is not configurable in v0.1. If an event should reach email, it should
be critical.
Environment
interface NotifyEnv {
NOTIFY_SLACK_WEBHOOK?: string
SLACK_WEBHOOK_URL?: string
SEND_EMAIL?: { send(message: unknown): Promise<unknown> }
RESEND_API_KEY?: string
NOTIFY_EMAIL_TO?: string
NOTIFY_EMAIL_FROM?: string
}| Channel | Required values |
| --- | --- |
| Slack | NOTIFY_SLACK_WEBHOOK (SLACK_WEBHOOK_URL accepted as a legacy fallback) |
| Email via Cloudflare | SEND_EMAIL, NOTIFY_EMAIL_TO, NOTIFY_EMAIL_FROM |
| Email via Resend | RESEND_API_KEY, NOTIFY_EMAIL_TO, NOTIFY_EMAIL_FROM |
If a requested and severity-allowed channel is missing required values,
notify() skips that channel with reason missing-binding. It does not throw.
Cloudflare Email
Cloudflare is the default email provider:
await notify(env, {
channels: ['email'],
severity: 'critical',
title: 'Digest failed',
body: 'The 2026-05-18 digest job failed.',
})Configure the Worker binding:
[[send_email]]
name = "SEND_EMAIL"Cloudflare Email requires the destination address to be verified through Email
Routing or Email Service setup before sends are accepted. If the binding rejects
with a destination-verification error, notify records that provider error in
result.failed[]; the package does not attempt verification.
Resend Fallback
Use Resend when the incident being reported could affect Cloudflare delivery itself:
await notify(env, {
channels: ['slack', 'email'],
severity: 'critical',
title: 'Cloudflare auth worker unavailable',
body: 'The monitor cannot reach the auth worker from two regions.',
emailProvider: 'resend',
})Resend sends a POST to https://api.resend.com/emails with bearer auth from
RESEND_API_KEY.
Slack Blocks
Slack payloads always include text: title as the fallback. If you do not pass
blocks, notify sends:
[
{
type: 'section',
text: { type: 'mrkdwn', text: `*${title}*\n${body}` },
},
]Pass blocks when the caller owns richer formatting. Email ignores blocks.
Dedup And Audit Hooks
Dedup is caller-owned:
await notify(env, {
channels: ['slack'],
severity: 'warning',
title: 'Queue backlog',
body: 'Email queue has been above threshold for 10 minutes.',
dedupKey: 'mailer:queue-backlog',
dedupFn: async (key) => alreadySentRecently(key),
})dedupFn is called per channel before dispatch. If it returns true, the
channel is skipped with reason deduped. If it throws, notify warns and sends
anyway. Over-delivery is safer than silently dropping an alert.
onSent runs once per successful adapter dispatch:
await notify(env, {
channels: ['slack', 'email'],
severity: 'critical',
title: 'Monitor failed',
body: 'The auth monitor detected a production outage.',
onSent: async (record) => writeAuditRecord(record),
})Errors from onSent are caught and logged with console.warn; a successful
send is not reversed by an audit failure.
Result Shape
interface NotifyResult {
attempted: Channel[]
sent: Channel[]
skipped: {
channel: Channel
reason: 'deduped' | 'missing-binding' | 'severity-routing'
}[]
failed: { channel: Channel; error: string }[]
}Input validation can throw for an invalid shape: empty channels, missing
title, missing body, unsupported severity, unsupported channel, or
unsupported email provider.
Adapter delivery failures never throw from notify(). They are captured in
failed[] and logged with console.warn.
Exports
export { notify } from '@growth-labs/notify'
export type {
Channel,
EmailProvider,
NotifyEnv,
NotifyInput,
NotifyResult,
SendEmailBinding,
SentRecord,
Severity,
SlackBlock,
} from '@growth-labs/notify'