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 seam —
sendEmail()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 returnsstatus: 'skipped'— the calling flow still works end to end. SettingRESEND_API_KEY+EMAIL_FROMenables real delivery via Resend (HTTPfetch, no SDK / no new dependency); swapping in SMTP/Gmail/SES later changes only this function. Integration secrets are read lazily here (not inenv.ts), matching the other adapters. - Welcome content is pure + branded — src/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 inoutbound_emails(migration0035) with the delivery status. The UI reportssentvssaved-but-not-connectedhonestly. 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_textstores 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.kindcan carry invoice/reminder emails later, retiring the unusedfacilities.email_template_*columns or feeding them through the seam.