Skip to main content

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:

  1. Wrong Vercel project (two repos exist — vars may be on abc-helper-app, not NPM-Helper-App).
  2. Wrong env target (must be Production, and the tested URL must be that Production deploy).
  3. Redeploy didn't apply the new vars (redeploy the latest Production build).
  4. 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 (settings keys email_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. getEmailStatusconnected | 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_id and stamps a threadable RFC Message-ID, persisted on outbound_emails at all send paths. Migration 0071_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 later 007x) before deploying this code. The new code writes provider_message_id / provider_message_rfc_id on outbound_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_FROM must be on NPM-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_id by email match), migration 0072.
  • 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_emails table + 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 into crm_activities/facility_notes.
  • Phase 9 (later) — full Gmail mailbox sync (repo has GMAIL_* scaffolding). Open: which mailbox (@abckidsny.com vs a @nightingalepm.com Workspace box).

4. Owner setup that gates inbound (Phases 4–7) — can start in parallel

  1. MX on a dedicated subdomain reply.nightingalepm.com (never the root — it would hijack the primary Google/MS mail), via Resend → Domains → Receiving; verify.
  2. Webhook at resend.com/webhooks → https://<prod>/api/webhooks/resend-inbound, event email.received; copy the whsec_… secret.
  3. Secrets: RESEND_WEBHOOK_SECRET=whsec_… in the deploy env (+ .env.example). Align outbound From/Reply-to to the reply.* 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.