Skip to content

feat(api): send notifier email through Postmark - #158

Merged
themightychris merged 4 commits into
developfrom
feat/postmark-notifier
Sep 8, 2026
Merged

feat(api): send notifier email through Postmark#158
themightychris merged 4 commits into
developfrom
feat/postmark-notifier

Conversation

@themightychris

Copy link
Copy Markdown
Member

Summary

Replaces the Resend email transport with Postmark. Postmark is what the legacy site already sends through, with the codeforphilly.org sender domain verified there; Resend was an unreviewed agent choice.

  • Specs/docs firstspecs/architecture.md (stack table + env table), specs/deferred.md, docs/operations/{secrets,deploy,cutover,cutover-announcement}.md, and the deploy/kustomize/base/configmap.yaml comment now describe Postmark.
  • Provider seamEmailNotifier now depends on a one-method EmailTransport interface (apps/api/src/notify/transport.ts). PostmarkTransport (postmark-transport.ts) is the only file that knows a vendor. The notifier contract is unchanged: LoggingNotifier fallback when unconfigured, log-and-delivered:false on any failure, same templates, same CFP_NOTIFICATION_FROM / CFP_SITE_HOST inputs.
  • EnvRESEND_API_KEYPOSTMARK_SERVER_TOKEN (optional secret); new POSTMARK_MESSAGE_STREAM (ConfigMap, default outbound).
  • Cutover mailout script drops its hand-rolled Resend fetch and reuses PostmarkTransport.
  • Tests — notifier suite stubs the transport seam; new postmark-transport.test.ts covers the field mapping with a stub and runs the real ServerClient against an MSW intercept of POST https://api.postmarkapp.com/email.

Postmark's SDK throws on every non-2xx instead of resolving { error }, so the notifier's four copy-pasted send blocks collapse into one #deliver with a single catch.

Plan: plans/postmark-notifier.md (closeout commit follows).

Operator follow-through (not in this PR)

  • Seal POSTMARK_SERVER_TOKEN in cfp-sandbox-cluster codeforphilly-ng.secrets/ (a server token from the existing Code for Philly Postmark account; one server per environment is recommended). Until then the pod keeps using LoggingNotifier, exactly as before.
  • Optionally set POSTMARK_MESSAGE_STREAM in the overlay if a stream other than outbound is wanted.
  • Remove the now-unused RESEND_API_KEY sealed secret if one was ever created.

Validation

  • npm run type-check clean (after npm run -w packages/shared build)
  • npm run lint clean
  • npm test: api 427/427, web 89/89, shared 75/75

🤖 Generated with Claude Code

https://claude.ai/code/session_01RdRwHvDupRLV8GuJpYKzEr

themightychris and others added 4 commits September 8, 2026 19:08
Postmark is what the legacy site already sends through, with the
codeforphilly.org sender domain verified there; Resend was an
unreviewed choice. Generated by:

    npm install -w apps/api postmark
    npm uninstall -w apps/api resend

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RdRwHvDupRLV8GuJpYKzEr
Postmark is the provider the legacy site already sends through, and the
codeforphilly.org sender domain is verified there; Resend was a
provider choice nobody reviewed. The notifier contract itself
(LoggingNotifier fallback when unconfigured, log-not-throw delivery,
CFP_NOTIFICATION_FROM / CFP_SITE_HOST) is unchanged. Env surface becomes
POSTMARK_SERVER_TOKEN (secret, optional) plus POSTMARK_MESSAGE_STREAM
(ConfigMap, default `outbound`).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RdRwHvDupRLV8GuJpYKzEr
Replace the Resend-backed EmailNotifier with a provider-neutral seam.
EmailNotifier now depends on a one-method `EmailTransport`
(`transport.ts`), and `PostmarkTransport` is the only file that knows
a vendor: it maps our from/to/subject/text/html onto Postmark's
PascalCase Message and tags the configured MessageStream. The notifier
contract is unchanged — LoggingNotifier fallback when unconfigured,
log-and-`delivered:false` on any failure, same templates, same
CFP_NOTIFICATION_FROM / CFP_SITE_HOST inputs.

Postmark's SDK throws on every non-2xx rather than resolving with an
`{ error }` envelope, so the notifier's four copy-pasted send blocks
collapse into one `#deliver` with a single catch; the logged `err`
carries Postmark's code/statusCode so operators can still tell a
rejected sender from a network blip.

Env: `RESEND_API_KEY` -> `POSTMARK_SERVER_TOKEN` (optional), plus
`POSTMARK_MESSAGE_STREAM` (default `outbound`). The cutover-mailout
script drops its hand-rolled fetch call and reuses PostmarkTransport.

Tests: the notifier suite stubs the transport seam directly; a new
postmark-transport suite covers the field mapping with a stub and the
real ServerClient against an MSW intercept of POST /email, so an SDK
wire-format change surfaces in CI instead of production.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RdRwHvDupRLV8GuJpYKzEr
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RdRwHvDupRLV8GuJpYKzEr
@themightychris
themightychris merged commit a1e83cf into develop Sep 8, 2026
1 check passed
@themightychris
themightychris deleted the feat/postmark-notifier branch September 8, 2026 23:38
@themightychris themightychris mentioned this pull request Sep 9, 2026
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.

1 participant