You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Research what a switch from Resend needs from posthog-node and a skill #522
What does a switch from Resend to PostHog need from posthog-node and from an agent skill, React Email templates included?
Michael wants transactional email from code to feel like Resend, so a team can switch with one skill: posthog.emails.send(...) mirrors the Resend Node SDK closely enough, and React Email templates work directly. It doesn't need a 100% mirror.
Answer, with links to Resend's docs and source (resend/resend-node, resend/react-email) and file:line evidence from the posthog-node draft PostHog/posthog-js#5312 (branch feat/node-emails-send on the Silthus fork):
Resend's send surface. The resend.emails.send fields (from, to, subject, html, text, react, replyTo, cc, bcc, headers, tags, attachments, scheduledAt, template), the { data, error } return, the error names and HTTP codes, the idempotency key, batch.send, and Resend's hosted templates with variables. Which of these does a typical transactional codebase use? Which does Think through the Elements tab PostHog/posthog#5312 already cover, and how close are the names?
React Email. How does Resend render react? Name the package, its peer dependencies, and whether it renders in the SDK or on the server. What do render() and toPlainText produce? Can a React Email component render once into a stored PostHog template whose per-send values are Liquid {{ variables.* }} placeholders, or does every send need its own render? Where does that break (conditionals, loops, formatting of props)? How do other providers that support React Email (for example Loops, Plunk, useSend, Postmark) handle it?
Keys and senders at Resend. Resend API key permissions (full_access, sending_access) and the per-domain restriction. How do Resend users keep staging from sending from the production domain? What do Resend's docs recommend?
Migration. What would a "move from Resend to PostHog" skill do, step by step, in a codebase: find the call sites, map fields, move templates, move domains and DNS (can a domain stay verified at both providers during the switch?), swap the key, and prove a send? Do Resend or its competitors publish such migration guides or agent skills? Link them.
Gaps. The smallest list of posthog-node additions that makes a typical Resend call site a mechanical rewrite.
Write the report on branch research/resend-parity-sdk. Resolve this ticket with a summary and the branch pointer.
Report: research-resend-parity-sdk.md on branch research/resend-parity-sdk (commit 8c794abc870). No PR, per the map's research convention.
Send surface.#5312 shares Resend's { data: { id }, error } return and the never-throws rule. Its input is { template, to, data, distinctId }, so it has no from, subject, html, text, react, replyTo or idempotency key. Resend's error field is name; the draft's is code. A public-code sample shows html: and react: as the most common content fields, replyTo and text common, and attachments, tags, hosted templates, cc, idempotency keys, batch and scheduling each rare.
React Email. Resend renders react in the SDK on every send, with a dynamic import of the optional peer @react-email/render (peers react and react-dom). The SDK sends HTML only; the Resend API builds the plain text. Rendering once into a stored {{ variables.* }} template works only for substitution-only emails. A local probe showed that quotes inside Liquid expressions are HTML-escaped, and JS conditionals, loops and formatting run at render time. Resend's own hosted templates have the same limit. Postmark, Plunk, useSend and Loops all treat React Email as a send-time renderer.
Keys. Resend keys are full_access or sending_access, plus a domain_id on sending keys. Resend recommends the narrowest scope, one key per service and one domain per key. No Resend page prescribes a staging or production split. PostHog's phs_ key has scopes only and no sender field, and a template's sender is fixed. Isolation today means one project per environment.
Migration. No competitor publishes a coding-agent skill for leaving Resend. Bird's "Hand this to your agent" prompt and Postmark's guide are the closest. The report lists an eight-step skill and the DNS collision points. Both providers' records can coexist if the MAIL FROM label differs from Resend's and the root SPF record is merged, not duplicated.
Gaps.from, inline subject + html/text (a backend decision), to as an array, replyTo, an idempotencyKey argument, error.name, and later react on top of inline content. cc, bcc, attachments, tags, scheduledAt, headers and batch stay out; the types should refuse them.
Two things for Michael:
Inline content or template-only decides whether a react: or html: call site is a one-line rewrite or a template conversion. The first reverses the 2026-10-16 re-chart.
The draft posts to /messaging/send/email/ and by_key routes that are not on upstream/master (2099bfc9e6f). That matches the planned by-key stack, not shipped code.
Not verified: how the React Email preview app maps props to {{{VAR}}} when it uploads to Resend, the error Resend returns for a domain-restricted key on another domain, and Resend's exact DNS record names.
The map's Decisions-so-far has a pointer to this answer.
Activity
Silthus commentedon Oct 20, 2026
Runtime: T3 Code
Task: node:delegated-task:command%3Amcp%3Acbc48805-d1ea-4076-9165-d9cb3e20ac16%3Adelegate-task%3Amap461-r522-v1
Child thread: thread:delegated-task:command%3Amcp%3Acbc48805-d1ea-4076-9165-d9cb3e20ac16%3Adelegate-task%3Amap461-r522-v1
Cast: Sonnet 5.5 high. Wall-time cap: 60 minutes.
Silthus commentedon Oct 20, 2026
Claimed by the research sub-agent (Sonnet 5.5 high). Report goes on branch
research/resend-parity-sdk.Silthus commentedon Oct 20, 2026
Resolution
Report:
research-resend-parity-sdk.mdon branchresearch/resend-parity-sdk(commit8c794abc870). No PR, per the map's research convention.{ data: { id }, error }return and the never-throws rule. Its input is{ template, to, data, distinctId }, so it has nofrom,subject,html,text,react,replyToor idempotency key. Resend's error field isname; the draft's iscode. A public-code sample showshtml:andreact:as the most common content fields,replyToandtextcommon, and attachments, tags, hosted templates,cc, idempotency keys, batch and scheduling each rare.reactin the SDK on every send, with a dynamic import of the optional peer@react-email/render(peersreactandreact-dom). The SDK sends HTML only; the Resend API builds the plain text. Rendering once into a stored{{ variables.* }}template works only for substitution-only emails. A local probe showed that quotes inside Liquid expressions are HTML-escaped, and JS conditionals, loops and formatting run at render time. Resend's own hosted templates have the same limit. Postmark, Plunk, useSend and Loops all treat React Email as a send-time renderer.full_accessorsending_access, plus adomain_idon sending keys. Resend recommends the narrowest scope, one key per service and one domain per key. No Resend page prescribes a staging or production split. PostHog'sphs_key has scopes only and no sender field, and a template's sender is fixed. Isolation today means one project per environment.from, inlinesubject+html/text(a backend decision),toas an array,replyTo, anidempotencyKeyargument,error.name, and laterreacton top of inline content.cc,bcc,attachments,tags,scheduledAt,headersand batch stay out; the types should refuse them.Two things for Michael:
react:orhtml:call site is a one-line rewrite or a template conversion. The first reverses the 2026-10-16 re-chart./messaging/send/email/andby_keyroutes that are not onupstream/master(2099bfc9e6f). That matches the planned by-key stack, not shipped code.Not verified: how the React Email preview app maps props to
{{{VAR}}}when it uploads to Resend, the error Resend returns for a domain-restricted key on another domain, and Resend's exact DNS record names.The map's Decisions-so-far has a pointer to this answer.