Skip to main content
The notifications module is infrastructure, not a messaging platform. It owns one canonical send model: a notification describes the delivery intent (recipient, channel, template, data) and a registered provider turns that into a transport-specific request and sends it. Specialized sends (invoices, payment sessions, reminders, booking documents) are orchestration wrappers over that one shared service, never separate notification systems. It ships as @voyant-travel/notifications, with a provider abstraction, first-party providers for local development and Voyant Cloud (email and SMS), database-backed templates and delivery logs, reminder rules and runs, and Hono routes.
pnpm add @voyant-travel/notifications

Key concepts

The architecture rests on a few clear ideas, kept consistent with the glossary verbs Deliver (push an issued artifact over a channel) and Issue (produce the artifact itself).
  • Templated notifications. Database-backed templates render with provider-agnostic data into transport fields (subject, text, html). Feature code chooses a template and a channel, not a vendor.
  • Multi-channel delivery. A notification names a channel (email, sms, and future channels such as push). Provider resolution happens in the infrastructure layer, by channel, not in feature code.
  • Providers. A provider is a transport implementation. It formats the vendor request, sends it, and returns a result in the shared shape. It does not own product business rules.
  • Channels. The delivery surface (email, SMS) that selects which registered provider sends a message.
  • Delivery. What was sent, through which channel, by which provider, with what result. Notifications guarantees only delivery intent and the transport result it actually knows, never a stronger promise than the provider can offer.
Notification delivery is transport, not CRM history. This module owns templates, delivery attempts, provider message ids, and reminder-oriented sends. Customer-relationship timelines belong to a higher-level product surface, not the transport layer.

What it owns

Providers, channels, and delivery

Providers implement the NotificationProvider contract. The module ships first-party providers (local console sink for development, Voyant Cloud email, Voyant Cloud SMS), and the bring-your-own path is first class: any project can implement NotificationProvider against raw Resend, Twilio, SES, or anything else and register it in place of the cloud adapters. When you register a set of providers, later providers override earlier ones on a channel conflict, and sendWith(name, payload) dispatches to a named provider directly.

Templates and delivery logs

Templates and delivery logs are database-backed (exposed through the ./schema and ./validation subpaths and managed through routes). Delivery logs record the transport result, channel, provider, and provider message id, which is the honest record of what the runtime actually did.

Reminders

Reminder rules drive scheduled operational sends and can currently target a booking_payment_schedule or an invoice. A scheduled sweep is run with sendDueNotificationReminders(...). Stage cadence and maxSendsInStage are evaluated against reminder run attempts: a queued, sent, skipped, or failed run consumes the stage slot, and a failed run is terminal until an operator or recovery flow requeues it.

Finance-aware and document sends

Specialized routes compose the core service for collection and document flows: sending a payment session or an invoice, listing a booking’s document bundle, and sending booking documents. These resolve recipients from the payment session, invoice, and linked booking travelers, then render the selected template with finance context such as payment links, invoice balances, and booking references. Booking document sends bundle the latest customer-facing contract attachment and the ready invoice or proforma rendition for a booking. Sensitive attachments resolve access at send time from durable storage metadata rather than relying on stale persisted signed URLs. Public, editorial assets may use stable public URLs, but private documents use signed or authenticated access. Override resolution with documentAttachmentResolver (or resolveDocumentAttachmentResolver) when mounting the routes so attachment URLs reflect the current runtime and storage context.

Booking document bundle lifecycle

The Hono module can subscribe to booking confirmation and fully-paid lifecycle signals through documentBundleLifecycle. The hook resolves the booking, the primary recipient, travelers, booking items, and the existing legal and finance document bundle before invoking the configured policy. The default policy is idempotent by document type: confirmation asks for contract plus proforma, and fully-paid asks for contract plus invoice. If a contract already exists from confirmation, the fully-paid hook records it as existing and only generates the missing invoice. Generator exceptions return as failed lifecycle results, and the subscriber logs booking id plus status only, never document contents or customer data.

Working with it

Create the service with a provider set and send a notification through the shared surface:
import { getVoyantCloudClient } from "@voyant-travel/voyant-cloud"
import { createNotificationService } from "@voyant-travel/notifications"
import { createLocalProvider } from "@voyant-travel/notifications/providers/local"
import { createVoyantCloudEmailProvider } from "@voyant-travel/notifications/providers/voyant-cloud-email"
import { createVoyantCloudSmsProvider } from "@voyant-travel/notifications/providers/voyant-cloud-sms"

const cloud = getVoyantCloudClient(env)
const notifications = createNotificationService([
  createLocalProvider({ channels: ["email"] }),
  createVoyantCloudEmailProvider({ client: cloud, from: "noreply@example.com" }),
  createVoyantCloudSmsProvider({ client: cloud }),
])

await notifications.send({
  to: "user@example.com",
  channel: "email",
  template: "welcome",
  subject: "Hello",
  html: "<p>Welcome</p>",
})
Mount the Hono module and resolve providers from the environment:
import {
  createNotificationsHonoModule,
  createVoyantCloudEmailProvider,
  createVoyantCloudSmsProvider,
} from "@voyant-travel/notifications"

const notificationsModule = createNotificationsHonoModule({
  resolveProviders: (env) => {
    const cloud = getVoyantCloudClient(env as Record<string, unknown>)
    return [
      createVoyantCloudEmailProvider({ client: cloud, from: "noreply@example.com" }),
      createVoyantCloudSmsProvider({ client: cloud }),
    ]
  },
})
Wire the document bundle lifecycle so confirmation and fully-paid signals generate and send the right documents:
const notificationsModule = createNotificationsHonoModule({
  resolveDb: (bindings) => getDbFromEnv(bindings),
  resolveProviders,
  documentBundleLifecycle: {
    enabled: true,
    confirmation: {
      notification: { templateSlug: "booking-confirmation" },
    },
    fullyPaid: {
      documentTypes: ["contract", "invoice"],
      notification: { templateSlug: "booking-paid-in-full" },
    },
    ensureLegalDocuments: async (context) => {
      await generateContractForBooking(context.booking.id)
    },
    ensureFinanceDocuments: async (context, request) => {
      await generateInvoiceDocuments(context.booking.id, request.documentTypes)
    },
  },
})
Run a reminder sweep on a schedule (from a workflow or cron):
import { sendDueNotificationReminders } from "@voyant-travel/notifications/tasks"

await sendDueNotificationReminders(db, process.env, {
  now: "2026-04-08T09:00:00.000Z",
})
Workflows, routes, and subscribers are all fine trigger points, but delivery should always converge on the shared notification service. Do not open-code provider-specific delivery in a workflow or feature module.
  • Finance. Payment-session and invoice sends, collection reminders, and the fully-paid document bundle all read finance context (payment links, invoice balances) and compose finance generators. See Finance.
  • Legal. Booking document sends bundle the latest customer-facing contract attachment; the confirmation policy generates a contract through ensureLegalDocuments. See Legal.
  • Bookings. Recipients resolve from linked booking travelers, and the document bundle lifecycle subscribes to booking confirmation and fully-paid signals.
  • Inventory. Product brochures stay an extension point via resolveBrochureDocuments, so apps that install @voyant-travel/inventory can add brochure artifacts without making notifications depend on products at runtime.

React package

@voyant-travel/notifications-react provides hooks, a client, query keys, reusable UI, and admin surfaces for template management, delivery listing, and reminder management.
import { NotificationsProvider } from "@voyant-travel/notifications-react/provider"
It exposes the standard family of subpaths (./hooks, ./client, ./query-keys, ./ui, ./admin, and ./components/*).

Next steps

Finance

The payment sessions, invoices, and schedules behind finance-aware sends.

Legal

The contracts bundled into booking document sends.

Workflows

Trigger reminders and follow-ups durably through the shared send surface.

Glossary

The Issue and Deliver verbs and the channel vocabulary.