Skip to main content

35. New-client welcome email + transactional send seam

Date: 2026-06-14

Status

Accepted

Context

After a facility is onboarded we want to send it a branded welcome email, pre-addressed to the people captured on the intake (authorized representative + administrator), with editable copy. The app had no email-sending mechanism — facilities carry email_template_* columns, but nothing in src delivers mail. So this is as much about establishing the wiring for transactional email as about the welcome message itself.

Decision

  • A single transactional-email seamsendEmail() in src/server/services/email.ts. It is the one place the app hands a message to a provider. No provider is configured by default, so it logs intent and returns status: 'skipped' — the calling flow still works end to end. Setting RESEND_API_KEY + EMAIL_FROM enables real delivery via Resend (HTTP fetch, no SDK / no new dependency); swapping in SMTP/Gmail/SES later changes only this function. Integration secrets are read lazily here (not in env.ts), matching the other adapters.
  • Welcome content is pure + brandedsrc/lib/email/welcome.ts builds the default (editable) subject/body and wraps the admin-edited plain-text body in a branded, inline-styled HTML template (brand colour #281d87, company wordmark header + footer). HTML is escaped, so the company name / body can't inject markup. Pure → unit-tested.
  • Flow — a "Send welcome email" button on the facility detail page opens a modal (WelcomeEmailButton) pre-populated with the intake's authorized-representative + administrator emails (deduped) and the default branded copy; every field is editable. sendWelcomeEmail (action) validates recipients (rejects malformed addresses), renders the branded HTML, calls the seam, and records every attempt in outbound_emails (migration 0035) with the delivery status. The UI reports sent vs saved-but-not-connected honestly.
  • outbound_emails — append-only record of transactional sends (recipients, subject, body, status, provider, error), company-scoped admin RLS + audit trigger; the basis for per-facility email history.

Consequences

  • The welcome flow is fully usable today (records + previews); turning on delivery is a one-time env change, no code change. Until then sends are recorded as skipped.
  • outbound_emails.body_text stores the message (not sensitive PII like SSN/bank); it is audited like other tables. The seam logs only counts/identifiers, never recipient bodies.
  • Reusable beyond onboarding: the seam + outbound_emails.kind can carry invoice/reminder emails later, retiring the unused facilities.email_template_* columns or feeding them through the seam.