Young's Mind
Implementation

Resend notification provider — the shipped email channel

Card: t_1419d598 (build task 3 of 8 from t_c9b50255). Method: live probes on prod (161.33.71.67, aarch64) against the pinned @medusajs/medusa 2.21.2, including a boot of the actual production image. Isolated scratch DB (resend_e2e_2026 on the pg-temp container). No tenant DB was written to.

Resend notification provider — the shipped email channel

Card: t_1419d598 (build task 3 of 8 from t_c9b50255). Method: live probes on prod (161.33.71.67, aarch64) against the pinned @medusajs/medusa 2.21.2, including a boot of the actual production image. Isolated scratch DB (resend_e2e_2026 on the pg-temp container). No tenant DB was written to.

1. What landed

File (on prod)Change
builds/medusa-backend/apps/backend/src/modules/resend/service.tsThe provider: AbstractNotificationProviderService, identifier = "notification-resend", one send().
builds/medusa-backend/apps/backend/src/modules/resend/index.tsModuleProvider(Modules.NOTIFICATION, { services: [...] }).
builds/medusa-backend/apps/backend/src/modules/resend/emails/layout.tsxShared shell + formatPrice + SummaryRow. Inline styles (see §4).
builds/medusa-backend/apps/backend/src/modules/resend/emails/order-placed.tsxOrder-confirmation template.
builds/medusa-backend/apps/backend/src/modules/resend/emails/order-shipped.tsxShipping-confirmation template.
builds/medusa-backend/apps/backend/package.jsonresend, @react-email/components, @react-email/render added; react + react-dom MOVED from devDependencies to dependencies.
builds/medusa-backend/package.jsonRoot overrides["@react-email/render"] = "2.1.1" — one copy of render in the tree.
builds/medusa-backend/apps/backend/medusa-config.tsComment corrected in place (see §5) — the code was already correct (card t_7b0702c4 owns that block).

Backups: package.json.bak-sol-t1419d598, package-lock.json.bak-sol-t1419d598, medusa-config.ts.bak-sol-t1419d598-comment.

2. The version matrix — MEASURED, not inherited

Medusa's Cloud guide pins @react-email/render 0.3.3 / components 1.1.4 and warns that newer React Email "are not supported due to Node version incompatibilities on Cloud". Cloud's ceiling is Cloud's. Our constraints are Node 24 + ARM64 + React 18/19, so the matrix was measured on our own stack.

PackagePinWhy this one
resend6.34.1 (exact)Latest measured working; MIT.
@react-email/components1.0.12 (exact)Measured OK. Cloud's 0.3.3 fails the Tailwind-utility translation (text-gray-800 does not resolve) — so Cloud's pin is not a floor, it is a ceiling.
@react-email/render2.1.1 (exact)Measured OK; components@1.0.12 otherwise nests 2.0.6 → 2 copies. Root overrides collapses it to 1.
react / react-dom18.3.1 (exact)In dependencies, not devDeps (see §3). Matches the monorepo root so npm hoists ONE copy.

Measured on prod, Node v24.19.0, aarch64:

resend                6.34.1  MIT
@react-email/components 1.0.12  MIT
@react-email/render   2.1.1   MIT
react                 18.3.1  MIT
react-dom             18.3.1  MIT

render copies in the tree: 1   (2.1.1)      npm ls @react-email/render -> exit 0

React 19 also works (the card asked for React 19/18 compatibility, proved not assumed). Isolated tree, react 19.2.0 + react-dom 19.2.0 + the same pinned render/components:

{"react":"19.2.0","react_dom":"19.2.0","render":"2.1.1","components":"1.0.12",
 "bytes":9585,"has_name":true,"has_total":true,"has_order_ref":true,
 "unresolved_jsx":false,"ok":true}

Both templates render real HTML (probe L, on the pinned stack):

order-placed  9892 bytes  doctype ✓  order-ref ✓  display_id ✓  total ✓  unresolved-jsx ✗
order-shipped 7587 bytes  doctype ✓  order-ref ✓  display_id ✓            unresolved-jsx ✗

3. react in dependencies, not devDependencies

The card called this out and it is real. The shipped Dockerfile (builds/Dockerfile.medusa) is a multi-stage build; the runner stage copies /app/node_modules from the builder. If react is a devDependency and the production install prunes dev deps, the provider dies at runtime with Cannot find module 'react/jsx-runtime'.

Proved in the built image:

/app/node_modules/react/jsx-runtime.js      <- resolves
react 18.3.1
react-dom 18.3.1
@react-email/render 2.1.1
@react-email/components 1.0.12

4. Two decisions that changed the implementation

4.1 The provider THROWS on failure — the card's "return {}" instruction is wrong

The card said: "on error log and return {} (do NOT throw — the module already records status=failure)".

That is false on the pinned v2.21.2. notification-module-service.js does:

const res = await this.notificationProviderService_.send(provider, entry.data)
  .catch((e) => { entry.data.status = FAILURE; ...; throw ... })
entry.data.external_id = res.id
entry.data.status = SUCCESS

It records status=failure only when send() rejects. A provider that swallows the error and returns {} yields status=success with external_id=NULL — a false success that a naive health gate would read as "email is alive". The shipped order.created subscriber swallows the error, so the DB row is the only evidence.

This provider therefore throws (MedusaError.Types.UNEXPECTED_STATE). A must-fail control was built and run against the same stub, the same DB and the same templates, with exactly one statement changed (return {} instead of throw) — it reproduced the false success. The card's instruction is overridden on measured evidence, and the override is documented in the provider's own class doc.

4.2 Inline styles, not Tailwind utilities

<Tailwind> translates class names to inline CSS at render time, and that translation has changed behaviour between React Email majors (measured: text-gray-800 resolves on components@1.0.12, not on the older 0.3.3). Inline styles are inert and render identically in every client. The styles object is explicitly annotated Record<string, CSSProperties> — see §6, that annotation is load-bearing, not decoration.

5. The shipped default STAYS none — and the config comment said otherwise

t_7b0702c4 left a comment in medusa-config.ts:

"When the provider modules land, switch the shipped default to resend in this file AND in the compose template (one line each)."

Following that instruction today would crash-loop the fleet. Probed in the real image:

TENANT_EMAIL_PROVIDER=resend, no RESEND_API_KEY
  -> "Loaders for module Notification failed: Option `api_key` is required
      in the Resend provider's options."          [BOOT FAILS]

TENANT_EMAIL_PROVIDER=resend, key, no RESEND_FROM_EMAIL
  -> "Option `from` is required ..."               [BOOT FAILS]

validateOptions runs at boot, not at send time. The module's existence resolves the original "missing module ⇒ boot failure" reason, but a missing credential is an equally fatal boot failure — and no Resend credential exists anywhere in the fleet (verified: no RESEND_API_KEY in any compose file, .env, or vault; the only key on prod is the literal placeholder re_probe_only_not_a_real_key).

So the default stays none — the tenant boots, feed works, and every email send fails loudly at SEND time. A loud failure is recoverable; a boot loop is an outage. The comment in medusa-config.ts was corrected in place to state the measured reason and the exact condition under which the default may flip (once the fleet has a provisioned credential AND onboarding sets RESEND_API_KEY / RESEND_FROM_EMAIL per tenant).

6. A build-breaker I introduced, and the Dockerfile that hid it

The first version of layout.tsx declared styles as an un-annotated object literal. textAlign: "center" widened to string, which fails React's CSSProperties → medusa build exited 1 with TS2322 on every template.

The shipped Dockerfile swallows build failures:

RUN npm run build 2>/dev/null || npx medusa build 2>/dev/null || echo "build attempted"

so the image still built — with a stale/absent compiled backend. Proved by A/B on the same tree:

(a) WITH    src/modules/resend -> exit 1  (TS2322, 4 errors)
(b) WITHOUT src/modules/resend -> exit 0  (backend + frontend built)

Fixed by annotating styles: Record<string, CSSProperties>. After the fix:

medusa build -> exit 0
  "Backend build completed successfully (25.76s)"
  "Frontend build completed successfully (78.17s)"
lint: src/modules/resend/** -> 0 problems

The || echo "build attempted" in the Dockerfile is a live trap — it makes a broken build look green. It is not this card's file to change; it is flagged in the card handoff for the image/infra lane.

7. Evidence — the acceptance criteria, all DB-verified

Probed twice: once in the source tree, once inside the built production image (solos-medusa:t1419d598-probe, NODE_ENV=production, Node 24.22.0 aarch64). The notification table afterwards, read from Postgres — not from logs, not from return values:

             id                | channel | status  | provider_id |   external_id   |     template
------------------------------+---------+---------+-------------+-----------------+----------------
 noti_01M5CQQBGR49V4GJP7VW75VQ0D | email   | success | resend      | stub-msg-000005 | order-placed
 noti_01M5CQQBW533B4NVVZ63HR7SER | email   | failure | resend      |                 | order-placed
 noti_01M5CQQBZXSTP0YT8MXRD1GJAJ | email   | failure | resend      |                 | order-placed
 noti_01M5CQQC525ACD20Q8NMY6ZSY7 | email   | failure | resend      |                 | no-such-template
 noti_01M5CQQC7SF33J27PGDYCTBN50 | feed    | success | local       |                 | t
 noti_01M5CQQC8VBPPY7F3HGHXBB771 | sms     | failure |             |                 | t

And the provider table — the registration criterion:

   id   | is_enabled | channels
--------+------------+----------
 local  | t          | {feed}
 resend | t          | {email}

Mapping to the criteria:

CriterionResultEvidence
Provider registers at boot, channels={email}, is_enabled=tPASSnotification_provider row resend / t / {email}
Send succeeds, row status=success, external_id setPASSsuccess + external_id=stub-msg-000005 (the Resend message id)
Failed send leaves status=failure — row, not logPASS3 failure rows: HTTP 422, socket destroyed, unknown template
feed still routes to localPASSfeed / success / local
Channel with no provider fails loudlyPASSsms → "Could not find a notification provider for channel: sms"
react/react-dom in dependenciesPASSreact/jsx-runtime resolves inside the built image

8. Honest limits — what is NOT proven

  • No real delivery test was performed. No email reached a real inbox. The Resend API was a local stub (127.0.0.1:9911, stub-resend.mjs) with the placeholder key re_probe_only_not_a_real_key. This proves the provider's behaviour, the module's recording of it, the version matrix, and the image — it does not prove deliverability, SPF/DKIM, or that Resend accepts our from address.
  • No verified domain exists. There is no Resend account, no domain, and no API key in the fleet. A live delivery test (send → a real inbox → confirm the row's external_id matches the Resend message id) is still owed and needs a credential from whoever owns the Resend account.
  • A template override (html_templates) was exercised only by unit reasoning, not by a probe. The order-placed / order-shipped paths are the ones measured.
  • The swallow-control (return {}) variant was proven on the stub; the claim that it yields a false success is measured, but the exact health-gate behaviour is the health-gate card's (t_7e2d75cb).
  • The order.placed → email wiring (a subscriber) is not in this card — it is t_eaa229fe. The shipped v2.21.2 order.created subscriber still references the non-existent order-created-template.
  • The O365 provider (t_154c2421) landed in the same tree during this run; medusa build passes with both modules present, but only Resend was booted and probed here.

9. Follow-ups this card leaves behind

  1. Provision a Resend account + verified domain + API key, then run the live delivery test (the only untestable item left on this card).
  2. Flip the default to resend in medusa-config.ts + both compose files, but only after (1) and after onboarding sets the two env vars per tenant.
  3. Fix the Dockerfile's || echo "build attempted" — it makes a broken build look green (it hid a real TS2322 failure in this very card).
  4. Consider not requiring from at boot: an operator who sets the key but forgets the sender gets a crash-loop instead of a loud send failure. The current behaviour is deliberate (fail closed at boot) but it is a fleet-wide footgun while the default is resend.

10. Files

Source of truth for the provider: builds/medusa-backend/apps/backend/src/modules/resend/ on prod (not a git repo — the build tree is a deployment copy).

Probe artifacts on prod: /home/ubuntu/resend-e2e/app/probeK.ts (live boot + e2e), /home/ubuntu/resend-e2e/app/probe-boot-only.ts (boot-only), /home/ubuntu/resend-e2e/stub-resend.mjs + stub-state.json (the stub and its request log).

Copyright © 2026