Handoff (continuation) — Email delivery + CRM email tracking
Date: 2026-06-21. Repo: NPM-Helper-App (canonical — not abc-helper-app, a diverged
clone that runs locally on :3000 without invoicing). Branch: work is on main's working tree,
uncommitted. Supersedes nothing — read alongside HANDOFF.md (original scope) and
CRM-EMAIL-TRACKING-PLAN.md (forward plan, Phases 2–9).
0. The "trouble sending invoices" diagnosis (resolved)
Symptom (owner, on the deployed app): "Recorded, but no email went out — email delivery isn't set up. Add RESEND_API_KEY + EMAIL_FROM (Resend), then Resend."
Sending is NOT broken. "Recorded" = the invoice was created/sent; the send_invoices RPC
succeeded. The only failure is the email seam returning skipped, which happens iff the running
process lacks both RESEND_API_KEY and EMAIL_FROM. The owner reports having added them to the
deploy + redeployed, yet it still skips → the runtime isn't seeing them. Ranked causes:
- Wrong Vercel project (two repos exist — vars may be on
abc-helper-app, notNPM-Helper-App). - Wrong env target (must be Production, and the tested URL must be that Production deploy).
- Redeploy didn't apply the new vars (redeploy the latest Production build).
- Name/value typo or only one of the two set (the seam needs both; no quotes in Vercel's field).
Local .env.local already has working keys (verified: real Resend send → HTTP 200). The old error
wording also proves the deploy predates this work (the fix reads "Configure it in Setup → Email
delivery").
1. What's BUILT and verified (uncommitted) — ready to commit/PR/deploy
tsc + Biome clean, 460 Vitest tests pass. Highlights:
- Configurable email delivery (
settingskeysemail_delivery,email_process; no secret in DB): company From / Reply-to / Enabled kill switch; area-based (per-process) reply-to — invoices →billing@, onboarding →onboarding@, recruiting, pipeline; per-send CC/BCC/reply-to in the invoice Review & send dialog (+ "Save as default"). Seam:sendEmail(provider primitive) +sendCompanyEmail(svc, companyId, msg, {process, cc, bcc, replyTo}). Reply-to precedence: per-send → area → company.getEmailStatus⇒connected | key_missing | disabled. - Sanity-check: 11 audit findings fixed (HIGH: per-send CC/BCC validation error was hidden in a
collapsed
<details>; per-process read/write isolation so one bad entry can't blank others; skip-reason "turned off" vs "not configured"; tighter sender validation; merged CC/BCC cap; blank- From no longer shadows env; stale "Saved." clears on edit). - Phase 1 (CRM tracking backbone): every send captures Resend's
provider_message_idand stamps a threadable RFCMessage-ID, persisted onoutbound_emailsat all send paths. Migration0071_outbound_emails_provider_ids.sql.
Key files: src/types/schemas/email.ts, src/lib/email/delivery.ts, src/server/services/email.ts,
src/server/services/settings.ts, src/server/actions/settings.ts, src/components/admin/EmailDeliveryForm.tsx,
src/components/admin/invoicing/ReviewAndSendDialog.tsx, src/app/admin/setup/page.tsx,
src/server/services/invoice/send.ts, src/db/migrations/0071_*.sql. Tests:
tests/lib/email/delivery.test.ts, tests/types/schemas/email.test.ts, tests/server/email-seam.test.ts.
2. ⚠️ Deploy gotchas (must-read before shipping)
- Migration order: apply
0071(and any later007x) before deploying this code. The new code writesprovider_message_id/provider_message_rfc_idonoutbound_emails; without the columns those inserts error (silently, best-effort) and email audit rows are lost. - Env vars on the right project:
RESEND_API_KEY,EMAIL_FROMmust be onNPM-Helper-App's Vercel Production env, then redeploy. (This is the §0 fix.) - Two repos: never build/deploy invoicing from
abc-helper-app.
3. Remaining plan (see CRM-EMAIL-TRACKING-PLAN.md)
Decisions locked with the owner (2026-06-21): inbound replies = Resend Inbound (GA, feasible); inbox link = BCC-to-CRM now, full Gmail sync later; AI note-taking — build it in.
- Phase 2 — per-individual attribution (
facility_contact_idby email match), migration0072. - Phase 3 — Facility 360 email timeline (facility-wide + per-individual). First owner-visible win.
- Feature I — invoice individualization: per-invoice + batch subject/body override.
- Phases 4–7 — inbound webhook (Svix-verified, not the Hubstaff token pattern) + receiving-
API fetch +
inbound_emailstable + reply threading (on the stamped Message-ID) + replies on Facility 360 + BCC-to-CRM address. Gated on owner setup (below). - Phase 8 — AI note-taking (
extractStructured, ADR-0026) summarizing logged threads intocrm_activities/facility_notes. - Phase 9 (later) — full Gmail mailbox sync (repo has
GMAIL_*scaffolding). Open: which mailbox (@abckidsny.comvs a@nightingalepm.comWorkspace box).
4. Owner setup that gates inbound (Phases 4–7) — can start in parallel
- MX on a dedicated subdomain
reply.nightingalepm.com(never the root — it would hijack the primary Google/MS mail), via Resend → Domains → Receiving; verify. - Webhook at resend.com/webhooks →
https://<prod>/api/webhooks/resend-inbound, eventemail.received; copy thewhsec_…secret. - Secrets:
RESEND_WEBHOOK_SECRET=whsec_…in the deploy env (+.env.example). Align outbound From/Reply-to to thereply.*subdomain so replies actually reach the webhook.
5. Run/verify locally
Run NPM-Helper-App's dev server (not abc-helper-app); .env.local has working Resend keys.
Apply migrations 0071+ to the dev DB first (Supabase) so the new columns exist. pnpm typecheck,
pnpm test, pnpm biome check all green.