Resend Studio
Public Made by Adomby adom
A local editor for Resend email templates: paste your API key, then edit, preview, lint, test-send, and publish templates without Resend's dashboard. Also imports SendGrid dynamic templates and converts their Handlebars to Resend's variable syntax.
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:
- Save writes a local draft (SQLite). Resend is untouched.
- Upload sends the draft to Resend. It creates the template, or updates Resend's DRAFT of it. Live email does not change.
- 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
stringornumber. - 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
subjectbuilt 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.
---
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
```bash
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.