main
Colby Knox Publish 0.1.0 fb6e12f 11d ago

name: adom-resend-studio description: Local server + Hydrogen webview for authoring Resend email templates (edit, live preview, lint, test send, upload, publish), plus a read-only SendGrid importer that converts Handlebars templates to Resend's {{{VAR}}} syntax. Use when editing a Resend template, migrating SendGrid templates to Resend, sending a test email, or publishing a template. Triggers - resend template, resend studio, email template editor, migrate sendgrid to resend, publish resend template, test email.

adom-resend-studio

Edit Resend templates without Resend's dashboard. Three layers, on purpose:

  1. Save writes a local draft (SQLite). Resend is untouched.
  2. Upload sends the draft to Resend. It creates the template, or updates Resend's DRAFT of it. Live email does not change.
  3. Publish makes Resend's draft the version real sends use. Needs a typed confirmation (the alias). Never publish unless the user said to.

Run

adom-resend-studio serve --port 8894 --open-tab   # --open-tab: Hydrogen tab; otherwise open http://127.0.0.1:8894
adom-resend-studio health --port 8894

Configure

The user pastes their own Resend API key in the studio's Settings (gear icon). Never ask them to paste a key into chat; point them at Settings. Keys live in ~/.local/share/adom-resend-studio/settings.json (owner-only) and are never returned by the API, only masked. A Resend key is either Full access (templates work) or Sending access (test sends only); GET /state reports resend_access as full, sending, invalid, unknown, or none.

Env vars win over Settings: RESEND_API_KEY, SENDGRID_API_KEY, RESEND_STUDIO_FROM, RESEND_STUDIO_REPLY_TO, RESEND_STUDIO_TEST_TO, RESEND_STUDIO_HOME (data folder), RESEND_STUDIO_DB, RESEND_STUDIO_ALLOWED_HOSTS (extra hostnames allowed to reach the server), SENDGRID_STUDIO_DB (carry sample data over from adom-sendgrid-studio).

Every write request must be Content-Type: application/json, and the Host must be 127.0.0.1/localhost (or an allowed host); anything else gets a 403.

Resend template rules (the lint enforces these)

  • Variables are {{{KEY}}} only. No {{#if}}, loops, or helpers.
  • Every variable used must be declared; up to 50; type string or number.
  • No fallback means required: Resend rejects a send that leaves it out.
  • Reserved keys: FIRST_NAME, LAST_NAME, EMAIL, UNSUBSCRIBE_URL, RESEND_UNSUBSCRIBE_URL, contact, this.
  • Only PUBLISHED templates can be sent by id/alias. The studio's "this draft" test mode renders locally and sends plain HTML, so drafts can be tested first.
  • Resend does NOT HTML-escape variables, and puts the same values into the subject. Senders should escape user-supplied values and pass their own subject built from the raw values.
  • An empty string satisfies a required variable; a missing one is a 422. Extra, undeclared variables are ignored.
  • GET /templates/{id} returns the PUBLISHED content, not Resend's draft.

SendGrid import

Each {{#if x}} block gets a mode: split (two templates, <alias>-with-x and <alias>-no-x; the sender picks), keep (always render; a fallback covers a missing value), or drop (never render; {{else}} content stays). {{#if x}}{{x}}{{else}}TEXT{{/if}} is detected and suggested as keep with fallback TEXT.

HTTP API (for agents)

Method Path Body / notes
GET/POST /api/settings masked keys + defaults; POST {resend_api_key?, sendgrid_api_key?, default_from?, default_reply_to?, default_test_to?} ("" clears)
GET /state connection + key access summary
GET /api/templates merged list: Resend templates + local drafts
POST /api/templates {name, alias} new local draft
GET /api/templates/{key} {editable, remote, lint, dirty, has_draft}
POST /api/templates/{key}/draft {name, alias, subject, from, reply_to, html, variables, notes} save locally
DELETE /api/templates/{key}/draft discard the local draft
POST /api/templates/{key}/upload {force?}; refuses on lint errors unless force
POST /api/templates/{key}/publish {confirm: "<alias>"}; refuses if local edits aren't uploaded
POST /api/templates/{key}/send-test {to, mode: "draft"|"published", values?}
POST /api/lint {html, subject, variables, alias}
POST /api/convert {html, subject, modes} preview a conversion
GET /api/sendgrid/templates SendGrid active versions + detected conditionals
POST /api/import/sendgrid {sendgrid_id, alias?, modes?, fallbacks?, samples?, overwrite?}
GET /console UI console log

A variable is {key, type, fallback, sample, description}; sample and description stay local.