Skip to content

feat(email): send through resend instead of postmark - #23

Merged
nutgood merged 2 commits into
mainfrom
feat/resend-email
Oct 21, 2026
Merged

nutgood merged 2 commits into
mainfrom
feat/resend-email

Conversation

@nutgood

@nutgood nutgood commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Moves transactional email (today: the invite flow in yucca-admin-api) from Postmark to Resend. FUTO's Resend account, sending domains and per-consumer keys are managed in immich-app/core-infra-tf (resend/futo-account); this app's key is YUCCA_RESEND_API_KEY, a sending_access key restricted to futo.cloud, which matches the prod from-address noreply@futo.cloud.

Client (@common/server/email)

  • POST /emails and POST /emails/batch with Authorization: Bearer, instead of Postmark's /email, /email/batch and X-Postmark-Server-Token.
  • Batches are chunked at 100 (Resend's limit, was 500).
  • Resend accepts or rejects a batch as a whole, so there are no per-message error codes any more: a 2xx marks every email in the chunk sent. A rejected chunk is logged and its recipients come back as failed (errorCode = the HTTP status), while earlier chunks keep their results. The invite flow then stamps only the delivered entries, so a retry can't resend invites that already went out.
  • The optional tag becomes a Resend tag category=<tag>. The Postmark-only MessageStream is dropped.
  • Env: RESEND_API_KEY (unset → log and skip, as before) and RESEND_API_URL (default https://api.resend.com), replacing POSTMARK_SERVER_TOKEN/POSTMARK_API_URL.

Dev mock: mock-postmark-provider is renamed to mock-resend-provider and speaks Resend's API, still delivering into Mailpit. Compose, Tilt, the dev charts and HelmReleases, build-check.yml, .env.example and the admin-api dev env defaults are updated to match, and so are docs/email.md and CLAUDE.md.

Deploying: the infrastructure repo has to pass RESEND_API_KEY in the yucca-admin-api Secret instead of POSTMARK_SERVER_TOKEN. That's a follow-up PR in immich-app/yucca, to merge once a release containing this one is running in prod. Until then, prod keeps sending through Postmark.

Tested locally:

  • the new email.repository.spec.ts drives the real repository against a mocked fetch: Resend paths, the bearer key, payload mapping and tags, 100-email chunking, and a rejected second chunk keeping the first 100 as sent;
  • allowlist.service.spec.ts passes;
  • @common/server type-checks;
  • eslint and Prettier pass on the changed files;
  • a smoke test of the mock (stand-in Mailpit): single and batch sends deliver with tags; a missing key gets 401; an invalid batch is rejected whole with 422; the old Postmark path returns 404.

Not tested against the real Resend API.

@nutgood
nutgood deployed to docs-preview October 21, 2026 18:13 — with GitHub Actions Active
@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

@futo-kritika

futo-kritika Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Re-runKritika Review

Switches transactional email and local mocks from Postmark to Resend.

1 finding · 1 P1

Confidence 3/5 · medium risk: The reported P1 stands: if this release runs before the Secret carries RESEND_API_KEY, the repository skips sends and reports success, so invites get stamped as sent without being delivered. The description's own deployment order (release first, Secret change after) hits this, and no Postmark fallback exists. Risk is medium because it is routine application logic that a revert undoes, limited to invite emails, and the Resend client has not been tested against the real API.

Not approved: confidence 3/5 is below the threshold of 4

Findings

Earlier findings (2 resolved)

Summary

The change migrates the email client, development mocks, and configuration to Resend. The latest revision preserves successful chunk results and adds direct client tests. The documented legacy-secret deployment sequence still skips delivery while recording invites as sent.

What's good

  • Per-chunk failure handling retains accepted recipients instead of discarding earlier successes.
  • New client tests exercise the HTTP contract and later-chunk rejection.
Review context · 0/6 commands
  • Commands run: none

Reviews (2) · Last reviewed commit: "fix(email): keep sent chunks when a late..." · openai/gpt-6.1-sol · $0.36

Comment on lines +45 to 47
if (!emailEnv.RESEND_API_KEY) {
this.logSkipped(messages);
return messages.map((message) => ({ to: message.to, errorCode: 0, message: 'Email sending disabled' }));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Preserve sending during the legacy-secret migration · correctness

When this release runs with only POSTMARK_SERVER_TOKEN, as the description's deployment sequence specifies, this branch skips sending and returns success for every recipient. AllowlistService.sendInviteEmails then persists inviteEmailSentAt for those unsent invites (allowlist.service.ts:97–109), so installing the Resend secret later does not make them retryable. Production does not keep sending through Postmark after this code is deployed: the old provider has been removed completely.

Suggested fix

Retain a temporary legacy Postmark fallback when only the old token is configured, until the secret migration is complete. Keep log-and-skip only for environments with neither provider configured, and document that migration order.

Prompt for a coding agent
In packages/common/src/email/email.repository.ts and packages/common/src/email/env.ts, retain a temporary Postmark compatibility path when POSTMARK_SERVER_TOKEN is configured but RESEND_API_KEY is absent. Use the Resend path when its key is present, and only use the existing log-and-skip behavior when neither provider is configured. Document the secret migration order in docs/email.md and add a regression test for an environment containing only the legacy token.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't give a f

Comment thread packages/common/src/email/email.repository.ts Outdated
Comment thread packages/common/src/email/email.repository.ts
@nutgood
nutgood deployed to docs-preview October 21, 2026 18:16 — with GitHub Actions Active
@nutgood
nutgood merged commit 6628ee2 into main Oct 21, 2026
20 checks passed
@nutgood
nutgood deleted the feat/resend-email branch October 21, 2026 18:18

This branch was successfully deployed

1 active deployment
docs-preview — 165f3c84 Deployed Oct 21, 2026 by nutgood via Deploy #27
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.

3 participants