Skip to content

vela-mail

Transactional email templates + transport package for VelaPay. Published as @velapay/mail (v0.2.0). Owns every customer-facing email VelaPay sends — auth verification, receipts, plan-change confirmations, cancellation notices, dunning, magic-link login.

Like vela-webhook, this is a package, not a standalone Worker. The actual delivery happens from the dashboard worker (or any other consumer Worker) using either Cloudflare Email Service (production) or Mailpit (local development).

Purpose and Role

  • Render every VelaPay transactional email from shared React Email components — one place to update brand, copy, and layout.
  • Provide a transport abstraction so consumers don't care whether they're hitting Cloudflare Email or a local Mailpit instance.
  • Carry brand tokens and typography from @vela/brand into emails so transactional mail looks like the rest of the product.
  • Be the consumer of vela-mail-smoke — every template renders + dispatches in CI before each release.

Tech Stack

TechnologyVersionPurpose
TypeScript^5.8.xLanguage
Bun1.3.xRuntime + bundler
React EmaillatestTemplate authoring (JSX → HTML email)
Cloudflare Email (send_email binding)Production transport
MailpitLocal-dev SMTP capture for visual review
BiomelatestLint + format

Built into dual CommonJS + ESM via build.ts.

Package Surface

ExportPurpose
send(template, props, transport)Render and dispatch a typed template via the configured transport
Template enumAll supported template IDs (e.g. auth.verification, invoice, plan.change, cancellation, dunning, receipt)
MailTransport interfacePluggable transport (Cloudflare Email production / Mailpit local)
Each template moduleAuthored as React Email JSX, exports subject + Component

Directory Structure

vela-mail/
├── emails/                    # React Email templates (JSX)
│   ├── auth-verification.tsx
│   ├── invoice.tsx
│   ├── plan-change.tsx
│   ├── cancellation.tsx
│   ├── dunning.tsx
│   ├── receipt.tsx
│   └── ...
├── src/
│   ├── send.ts                # Template + transport dispatch
│   ├── transports/
│   │   ├── cloudflare.ts      # CF send_email binding
│   │   └── mailpit.ts         # Local SMTP for development
│   └── index.ts
├── tests/                     # bun:test rendering + dispatch tests
├── COPY-REVIEW.md             # Track when copy was last reviewed per template
├── build.ts
├── package.json
└── tsconfig.json

Consumers

ConsumerWhat it sends
vela-dashboard (worker)Invoice and payment-receipt emails, plan-change confirmations, cancellation notices, dunning, team invites
vela-portal (worker)Magic-link login, subscription state-change confirmations
vela-mail-smokeAll templates — exercises render + dispatch end-to-end before release

Commerce payment truth, refund/credit-note PDFs, and document persistence remain in vela-dashboard; vela-mail owns typed email presentation and transport, not refund or reconciliation state.

Deployment Target

  • Workspace package — linked into consumer Workers via file:../vela-mail.
  • Production transport — Cloudflare Email Service send_email binding. Consumer Workers declare allowed sender addresses (e.g. mail@velapay.io, billing@velapay.io) in their wrangler.toml.
  • Local transport — Mailpit catches outbound mail during development. No real mail is sent locally.

Notable Design Decisions

React Email

Templates author in JSX with the same component model as the rest of the workspace. Rendering happens at send time inside the Worker — no separate template-compile step. Designers preview templates by running the React Email dev server locally.

Transport Abstraction

Production uses send_email Worker bindings (Cloudflare Email Service). Local development routes to Mailpit. The same send() API works in both — no environment-specific branching at the call site.

Allowed-Sender Per Template Type

Cloudflare Email requires per-binding allowed-sender addresses. The wiring is per-Worker (not per-template) — consumer wrangler.toml files declare bindings (e.g. AUTH_EMAIL mapped to mail@velapay.io, BILLING_EMAIL mapped to billing@velapay.io) and vela-mail selects the right binding based on template type.

Co-Versioned with Brand

Email visual language tracks @vela/brand versions. A brand bump that changes typography or color requires a parallel vela-mail release so transactional mail doesn't drift from the product UI.

Internal knowledge base for the Vela Labs workspace.