Skip to content

feat(api): EmailNotifier — Resend-backed help-wanted notifications (closes #82)#98

Merged
themightychris merged 5 commits into
mainfrom
feat/notifier-email
May 30, 2026
Merged

feat(api): EmailNotifier — Resend-backed help-wanted notifications (closes #82)#98
themightychris merged 5 commits into
mainfrom
feat/notifier-email

Conversation

@themightychris
Copy link
Copy Markdown
Member

Summary

Replaces the no-op `LoggingNotifier` with a real Resend-backed implementation for help-wanted-role notifications, per `specs/behaviors/help-wanted-roles.md`. Email-only first cut; Slack DM is split off to #95 because it needs deeper Slack-integration trust.

What's new

  • `apps/api/src/notify/templates.ts` — pure-function renderers for both notification kinds (interest + filled). Plain-text + HTML, with HTML-escaped interpolation of user-supplied fields.
  • `apps/api/src/notify/email-notifier.ts` — Resend SDK wrapper handling both failure modes (SDK throws + `{ error }` response shape). `delivered: false` in either case; the route still returns 202.
  • `services` plugin: installs `EmailNotifier` when `RESEND_API_KEY` is set, falls back to `LoggingNotifier` otherwise. Dev + tests stay configuration-free.
  • `env.ts`: `RESEND_API_KEY` (optional secret), `CFP_NOTIFICATION_FROM` (defaults to `"Code for Philly notifications@codeforphilly.org"`).
  • `docs/operations/secrets.md`: `RESEND_API_KEY` rotation procedure + the SPF/DKIM/DMARC pre-flight requirement on the sender domain (unverified domains hard-bounce or spam-filter immediately).

Test plan

  • 11 new test cases in `apps/api/tests/email-notifier.test.ts` covering template renderers (with + without message body, HTML escaping of user-supplied fields) and notifier paths (Resend success, Resend-reported error, SDK throw, missing maintainer email → no Resend call).
  • 302/302 API tests pass; `npm run type-check && npm run lint` clean.
  • Sandbox smoke (deferred to deploy) — set `RESEND_API_KEY` in the cluster sealed-secret, verify sender domain in Resend, express interest on a real help-wanted role, confirm the maintainer's inbox lights up.

Closes #82.

🤖 Generated with Claude Code

themightychris and others added 5 commits May 29, 2026 22:22
Plan was queued (status: planned) in PR #96; flipping to in-progress
as we start the implementation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Installed via:
  npm install --workspace=apps/api resend

Resend ships an HTTPS-API SDK (no SMTP); aligned with the plan's
choice of transport for the help-wanted notifier. When
RESEND_API_KEY is unset (tests, dev without an account), the
notifier falls back to LoggingNotifier so existing code paths
keep working without configuration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…loses #82)

Replaces the LoggingNotifier no-op with a real email notifier for the
two help-wanted notification kinds the Notifier interface declares
(notifyHelpWantedInterest, notifyHelpWantedFilled).

Implementation:
  - apps/api/src/notify/templates.ts — pure-function template renderers
    for both notification kinds. Plain-text + HTML alternatives, with
    HTML-escaped interpolation of user-supplied fields (role title,
    full name, message body). External-link transform from #91 doesn't
    apply here — emails carry absolute URLs to the site host.
  - apps/api/src/notify/email-notifier.ts — Resend-API-backed class.
    Translates {data, error} response shape: throwing SDK + Resend-
    reported errors + missing-email all return delivered:false; the
    route still returns 202 to the caller per spec.
  - services plugin: installs EmailNotifier when RESEND_API_KEY is set,
    otherwise falls back to LoggingNotifier. Means dev + tests stay
    configuration-free; existing tests untouched.
  - env.ts: RESEND_API_KEY (optional secret), CFP_NOTIFICATION_FROM
    (defaults to "Code for Philly <notifications@codeforphilly.org>").

11 new tests: template assertions (interest + filled, with + without
message, HTML-escape of user-supplied fields), notifier paths (Resend
success, Resend-reported error, SDK throw, missing email → no Resend
call). 302/302 API tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  - .env.example: section for both vars with dev-friendly defaults +
    "leave unset to use LoggingNotifier" note.
  - kustomize base configmap: CFP_NOTIFICATION_FROM baked in; the
    sealed-secret for RESEND_API_KEY is operator-supplied per env.
  - docs/operations/deploy.md env table: two new rows.
  - docs/operations/secrets.md: RESEND_API_KEY rotation procedure +
    the SPF/DKIM/DMARC pre-flight requirement on the sender domain
    (unverified domains get spam-filtered immediately).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closeout: flip to done, tick all validations (11 new tests + 302/302
API tests, lint + type-check clean), fill Notes (HTML escaping was
load-bearing, Resend SDK's two failure shapes, unset-API-key fallback
kept existing tests passing, dual text+html bodies) + Follow-ups
(newsletter export endpoint, bounce/complaint webhooks, PII redaction
in logs, Slack DM via #95, templating engine when 3+ notifications
exist).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@themightychris themightychris merged commit 73aff13 into main May 30, 2026
1 check passed
@themightychris themightychris deleted the feat/notifier-email branch May 30, 2026 02:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

behavior: real notifier for help-wanted-roles (replace LoggingNotifier stub)

1 participant