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

@venturekit/notify

v0.0.0-dev.20260507015944

Published

Transactional notifications for VentureKit — email (SES), WhatsApp (Meta Cloud), with a Postgres outbox + cron dispatcher.

Downloads

199

Readme

@venturekit/notify

Transactional notifications for VentureKit applications — email (AWS SESv2), WhatsApp (Meta Cloud API), with SMS / push as placeholders. Backed by a Postgres outbox + auto-provisioned cron dispatcher Lambda.

Pre-release — API may change before 1.0.

What you get

Declare one notify intent in vk.config.ts and vk deploy provisions:

  • SES email identity (single-address or whole-domain, with DKIM signing when domain-based)
  • SES configuration set with bounce / complaint / delivery / open / click events streaming into an SNS topic
  • SNS bounce-handler Lambda that records every event in notification_event_log and auto-suppresses hard-bounced addresses
  • Postgres outbox schema (notifications, notification_preferences, notification_event_log) shipped with the package and merged into the project's migration runner
  • EventBridge cron + dispatcher Lambda that drains the outbox once per minute (configurable) and calls each row's provider
  • Secrets Manager secret for the Meta WhatsApp API token (operator pastes the value after first deploy)
  • IAM grants (ses:SendEmail, ses:SendRawEmail, secretsmanager:GetSecretValue) scoped to the resources above on the shared Lambda execution role

Application code only touches notify.enqueue({...}). Retry, preference filtering, address suppression, bounce processing, and template rendering all happen behind the scenes.

Quick start

1. Declare the intent

// vk.config.ts
import { defineVenture } from '@venturekit/infra';

export default defineVenture({
  base,
  security,
  databases: [{ id: 'main', type: 'postgres', name: 'app' }],
  notify: [
    {
      id: 'main',
      channels: ['email', 'whatsapp'],
      defaultFrom: '[email protected]',
      domain: 'app.com',           // optional: SES verifies the whole domain
      hostedZoneId: 'Z123…',       // optional: auto-publish DKIM CNAMEs in Route53
      whatsapp: {
        phoneNumberId: '105954992345678', // from Meta Business Suite
      },
    },
  ],
  envs: { dev, prod },
});

2. Build the runtime client

// src/lib/notify.ts
import {
  createNotifyClient,
  createPostgresNotifyStore,
  createSesEmailProvider,
  createMetaWhatsAppProvider,
  createSecretsTokenResolver,
} from '@venturekit/notify';
import { findUserById } from '@/db/repos/users';

export const notify = createNotifyClient({
  store: createPostgresNotifyStore(),
  defaultFrom: process.env.VENTURE_NOTIFY_FROM,
  providers: [
    createSesEmailProvider({
      region: process.env.AWS_REGION!,
      configurationSetName: process.env.VENTURE_NOTIFY_CONFIG_SET,
    }),
    createMetaWhatsAppProvider({
      phoneNumberId: process.env.VENTURE_NOTIFY_WHATSAPP_PHONE_ID!,
      tokenResolver: createSecretsTokenResolver({
        secretArn: process.env.VENTURE_NOTIFY_WHATSAPP_TOKEN_SECRET!,
      }),
    }),
  ],
  // Map user ids -> channel-specific addresses. Phase 1 expects the
  // consumer to own this lookup; future phases may add a default that
  // reads from a user table convention.
  addressResolver: async (userId, channel) => {
    const user = await findUserById(userId);
    if (channel === 'email')    return user?.email ?? null;
    if (channel === 'whatsapp') return user?.whatsappNumber ?? null;
    return null;
  },
});

// Templates live in code in Phase 1. Register them at module load so
// the dispatcher's first invocation already sees them.
notify.registerTemplate({
  key: 'application_registered',
  channel: 'email',
  category: 'transactional_application',
  render: ({ groupName }: { groupName: string }) => ({
    subject: `Inscription confirmée: ${groupName}`,
    text: `Votre inscription au groupe ${groupName} est confirmée.`,
    html: `<p>Votre inscription au groupe <b>${groupName}</b> est confirmée.</p>`,
  }),
});

VentureKit's auto-provisioned dispatcher imports this module via VENTURE_NOTIFY_CLIENT_MODULE (default ./lib/notify.js). Override the env var per intent if your project uses a different path.

3. Send

// src/routes/applications/post.ts
import { notify } from '@/lib/notify';

await notify.enqueue({
  recipientUserId: ctx.user.id,
  channel: 'email',
  templateKey: 'application_registered',
  payload: { groupName: 'Île-de-France #4' },
  relatedKind: 'application',
  relatedId: applicationId,
});

The handler returns synchronously after the row hits the outbox. The dispatcher cron picks it up on the next tick (≤ 1 minute by default) and calls SES.

Architecture

┌────────────────────┐    ┌──────────────────────┐    ┌──────────────────┐
│  Route handler     │    │  Outbox (Postgres)   │    │  Dispatcher cron │
│ notify.enqueue()   │───►│  notifications       │◄───│  every 1 minute  │
└────────────────────┘    │  + preferences       │    │  drain pending   │
                          │  + event_log         │    │  call provider   │
                          └──────────────────────┘    │  mark sent/fail  │
                                                      └────┬─────────────┘
                                                           │
              ┌────────────────────────────────────────────┴─────┐
              ▼                                                   ▼
    ┌──────────────────────┐                       ┌──────────────────────┐
    │  AWS SESv2           │                       │  Meta Graph API      │
    │  SendEmail           │                       │  /messages           │
    └──────────┬───────────┘                       └──────────────────────┘
               │
               ▼
    ┌──────────────────────┐
    │  SES events SNS      │
    │  bounce / complaint  │──────► Bounce handler Lambda ──► event_log
    └──────────────────────┘                                     │
                                                                 ▼
                                                           Address suppressed
                                                           in future sends

Three persistent failure modes are recorded distinctly:

| Status | Meaning | Retried? | | --- | --- | --- | | pending | Awaiting next cron tick. | Yes — claimed by claimPending(). | | sent | Provider accepted delivery. | No. May still flip to bounced. | | failed | Provider rejected (hard error) or transient retries exhausted. | Manually via admin retry. | | bounced | Bounce / complaint event arrived after sent. | Address auto-suppressed. | | cancelled | Suppressed at enqueue (opt-out, address suppression). | No. |

Channels

| Channel | Phase 1 provider | Real impl? | | --- | --- | --- | | email | AWS SESv2 (@aws-sdk/client-sesv2, lazy-loaded) | ✅ | | whatsapp | Meta Cloud API (Graph /messages, fetch) | ✅ | | sms | noop-sms placeholder — logs and returns success | ❌ — register a real provider in your createNotifyClient({ providers }) array. | | push | noop-push placeholder | ❌ — same as above. |

The dispatcher uses whichever provider is registered last for a given channel — wire your own SNS / Twilio / Pinpoint provider by passing it in the providers array; it overrides the noop.

Outbox tracking

Phase 1 persistence is always Postgres. NotifyIntent.tracking exists in the type for forward-compatibility but only 'postgres' is implemented. Auto-applied migrations create:

  • notifications — outbox / audit log
  • notification_preferences — per-(user, channel, category) opt-in
  • notification_event_log — bounce / complaint / delivery / open / click events

Migrations live under node_modules/@venturekit/notify/migrations/vk_notify_*.sql. The infra package merges them into the project's CodeBuild migration runner asset on every vk deploy. Filename collisions with project-shipped migrations throw at synth time — VentureKit packages prefix their files with vk_<package>_ to avoid that hazard.

Templates

Phase 1 keeps templates in code. Each template is a typed (payload) -> { subject?, text, html?, whatsapp? } function registered at boot:

notify.registerTemplate<{ name: string }>({
  key: 'welcome',
  channel: 'email',
  category: 'transactional_account',
  render: ({ name }) => ({
    subject: `Welcome, ${name}`,
    text: `Hi ${name}, welcome aboard.`,
    html: `<p>Hi <b>${name}</b>, welcome aboard.</p>`,
  }),
});

Templates without a category bypass the per-user opt-out check — useful for password resets, security alerts, etc., that the user can never opt out of.

WhatsApp template messages need Meta-pre-approved templates and use the RenderedMessage.whatsapp block:

notify.registerTemplate({
  key: 'application_received',
  channel: 'whatsapp',
  category: 'transactional_application',
  render: ({ name, groupName }: { name: string; groupName: string }) => ({
    text: `Hi ${name}, your application to ${groupName} is in.`,
    whatsapp: {
      metaTemplateName: 'application_received_v1',
      languageCode: 'fr',
      components: [
        {
          type: 'body',
          parameters: [
            { type: 'text', text: name },
            { type: 'text', text: groupName },
          ],
        },
      ],
    },
  }),
});

Phase 2 will introduce DB-backed templates with admin UI editing — see the project roadmap.

Admin / inbox helpers

@venturekit/notify/admin exposes route-helper functions consumers wire into their own handlers (no automatic mounting — pick your own auth/scope conventions):

// src/routes/admin/notifications/list/get.ts
import { handler } from '@venturekit/runtime';
import { adminListNotifications } from '@venturekit/notify/admin';
import { notify } from '@/lib/notify';

export const main = handler(async (ctx) => {
  ctx.requireScope('admin.notifications.read');
  const url = new URL(ctx.request.url);
  return adminListNotifications(notify, {
    limit: Number(url.searchParams.get('limit') ?? 50),
    offset: Number(url.searchParams.get('offset') ?? 0),
    status: url.searchParams.get('status') as never,
    search: url.searchParams.get('q') ?? undefined,
  });
});

Available helpers:

  • adminListNotifications(client, options) — paginated list with status / channel / template / search filters
  • adminGetNotification(client, id)
  • adminSendNotification(client, input) — equivalent to client.enqueue(...), separate name for audit logging
  • adminRetryNotification(client, id) — flips failed / bounced back to pending
  • adminCancelNotification(client, id) — cancels a pending row before dispatch
  • adminClearSuppression(client, channel, address) — lifts a bounce / complaint suppression
  • listUserPreferences(client, userId) / upsertUserPreference(client, ...) — per-user opt-in management
  • listNotificationsForUser(client, userId, options) — buyer-portal inbox view

Local dev (vk dev)

Declaring a notify intent with the email channel adds a MailHog service to ~/.vk/docker-compose.yml:

  • SMTP at localhost:1025
  • Web inbox at http://localhost:8025

VK's local-mode runtime injects VENTURE_NOTIFY_DRIVER=local so the email provider falls through to the noop / log adapter — Phase 1 doesn't auto-route through MailHog yet (planned for Phase 2). Until then operators can spin a custom email provider against MailHog by passing endpoint: 'http://localhost:1025' in createSesEmailProvider (the SES SDK lets you point at any SMTP, MailHog accepts).

Environment variables

The infra package injects these on every Lambda when a notify intent is declared:

| Variable | Source | | --- | --- | | VENTURE_NOTIFY_FROM | First notify intent's defaultFrom | | VENTURE_NOTIFY_CONFIG_SET | First notify intent's SES configuration set name | | VENTURE_NOTIFY_EVENTS_TOPIC_ARN | First notify intent's SES events SNS topic ARN | | VENTURE_NOTIFY_WHATSAPP_PHONE_ID | First notify intent's whatsapp.phoneNumberId | | VENTURE_NOTIFY_WHATSAPP_API_VERSION | First notify intent's whatsapp.apiVersion | | VENTURE_NOTIFY_WHATSAPP_TOKEN_SECRET | Secrets Manager ARN for the WhatsApp token | | NOTIFY_<ID>_FROM / NOTIFY_<ID>_CONFIG_SET / NOTIFY_<ID>_EVENTS_TOPIC_ARN | Per-id overrides for projects with multiple intents | | VENTURE_NOTIFY_BATCH_SIZE | Dispatcher cron — overrides default 50 | | VENTURE_NOTIFY_CLIENT_MODULE | Path to the consumer's notify-client module (default ./lib/notify.js) |

Migrating from @venturekit-pro/comms

The 0.x cut of @venturekit-pro/comms shipped email + push + SMS + WhatsApp + chat in one package. The split:

  • Notifications (this package, free tier): all four channels (email / WhatsApp / SMS / push), templates, preferences, outbox, dispatcher.
  • Chat (@venturekit-pro/chat, Pro): rooms, participants, messages, presence — unchanged API.

Migrate with one install:

pnpm add @venturekit/notify @venturekit-pro/chat
pnpm remove @venturekit-pro/comms

Then update imports — the chat manager API is unchanged, the notify API is intent-driven (declare in vk.config.ts, get auto-provisioned SES + outbox + cron).

License

Apache-2.0