Appearance
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/brandinto 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
| Technology | Version | Purpose |
|---|---|---|
| TypeScript | ^5.8.x | Language |
| Bun | 1.3.x | Runtime + bundler |
| React Email | latest | Template authoring (JSX → HTML email) |
Cloudflare Email (send_email binding) | — | Production transport |
| Mailpit | — | Local-dev SMTP capture for visual review |
| Biome | latest | Lint + format |
Built into dual CommonJS + ESM via build.ts.
Package Surface
| Export | Purpose |
|---|---|
send(template, props, transport) | Render and dispatch a typed template via the configured transport |
Template enum | All supported template IDs (e.g. auth.verification, invoice, plan.change, cancellation, dunning, receipt) |
MailTransport interface | Pluggable transport (Cloudflare Email production / Mailpit local) |
| Each template module | Authored 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.jsonConsumers
| Consumer | What 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-smoke | All 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_emailbinding. Consumer Workers declare allowed sender addresses (e.g.mail@velapay.io,billing@velapay.io) in theirwrangler.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.
Related
- Smoke harness: vela-mail-smoke →
- Brand source: vela-brand →
- Email-primary auth model: Auth Model Decision →