Skip to content

@venturekit/notify

@venturekit/notify provides transactional messaging — email (AWS SESv2), WhatsApp (Meta Cloud API), with SMS and push as pluggable placeholders. It is intent-driven: declare a notify intent in vk.config.ts and vk deploy auto-provisions an SES identity + configuration set, a bounce/complaint handler, a Postgres outbox, and an EventBridge cron dispatcher. Application code only calls notify.enqueue(...).

Terminal window
npm install @venturekit/notify@dev
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: 'no-reply@app.com',
domain: 'app.com', // optional: SES verifies the whole domain (DKIM)
hostedZoneId: 'Z123ABC', // optional: auto-publish DKIM CNAMEs in Route 53
whatsapp: { phoneNumberId: '105954992345678' },
},
],
envs: { dev, prod },
});

See Infrastructure Intents for the full intent shape.

src/lib/notify.ts
import {
createNotifyClient,
createPostgresNotifyStore,
createSesEmailProvider,
createMetaWhatsAppProvider,
createSecretsTokenResolver,
} from '@venturekit/notify';
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 (consumer-owned in Phase 1).
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 at module load.
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>`,
}),
});

The auto-provisioned dispatcher imports this module via VENTURE_NOTIFY_CLIENT_MODULE (default ./lib/notify.js).

import { notify } from '@/lib/notify';
await notify.enqueue({
recipientUserId: ctx.user!.id,
channel: 'email',
templateKey: 'welcome',
payload: { name: 'Alice' },
relatedKind: 'account',
relatedId: accountId,
});

enqueue() returns synchronously after the row lands in the outbox. The dispatcher cron drains it on the next tick (≤ 1 minute by default) and calls the provider.

ChannelPhase 1 providerReal impl?
emailAWS SESv2 (lazy-loaded)Yes
whatsappMeta Cloud API (Graph /messages)Yes
smsnoop placeholderNo — register your own provider
pushnoop placeholderNo — register your own provider

Persistence is Postgres. Auto-applied migrations (vk migrate / vk deploy) create notifications, notification_preferences, and notification_event_log. Each notification moves through these statuses:

StatusMeaningRetried?
pendingAwaiting the next cron tickYes
sentProvider accepted deliveryNo (may flip to bounced)
failedHard error or retries exhaustedManually via admin retry
bouncedBounce/complaint event arrived after sentAddress auto-suppressed
cancelledSuppressed at enqueue (opt-out/suppression)No
SubpathDescription
@venturekit/notifyClient, providers, templates, store, types
@venturekit/notify/adminAdmin / inbox route helpers (adminListNotifications, adminRetryNotification, …)
@venturekit/notify/dispatcherdispatchOnce, makeDispatchHandler (cron Lambda)
@venturekit/notify/bounce-handlermakeBounceHandler (SNS bounce/complaint Lambda)
@venturekit/notify/providers/email-sesSES email provider
@venturekit/notify/providers/whatsapp-metaMeta WhatsApp provider
@venturekit/notify/store/postgresPostgres outbox store
@venturekit/notify/migrationsOutbox migration SQL (auto-merged by @venturekit/infra)

Declaring a notify intent with the email channel adds a MailHog service to your local Docker Compose (SMTP on localhost:1025, web inbox at http://localhost:8025).

  • @venturekit/core — required
  • @venturekit/data — optional peer (Postgres outbox store)
  • @venturekit/runtime — optional peer (dispatcher / handler wiring)
  • @aws-sdk/client-sesv2 — optional peer (SES email)
  • @aws-sdk/client-secrets-manager — optional peer (WhatsApp token)