A generation-first newsletter studio. Describe a newsletter and let AI draft it, design it live with DESIGN.md tokens, and send it to a subscriber list you own. Built with React + Tailwind CSS + Hono + D1, deploys to Cloudflare Workers via Clawnify.
A self-hostable, open-source alternative to Mailchimp, beehiiv, Substack, and ConvertKit — the AI editor, the brand controls, and the sending pipeline, fully yours. No per-subscriber pricing, no lock-in.
Your contacts live in your own database, not in a sending provider's account. The provider is just delivery, and it's swappable.
- Generation-first editor — write a prompt, get a structured draft (eyebrow, title, deck, Markdown body); revise it in place.
- Live DESIGN.md design panel — a Basic/Advanced token editor (colors, typography, layout, sections) that re-renders the preview as you type. The same tokens drive the sent email.
- Desktop / Mobile preview — a faithful canvas that mirrors the email renderer exactly.
- Template library — three shipped looks (Classic Editorial, Minimal Mono, Bold Bulletin); Save as… turns any mail into your own template.
- Your subscriber list, in your database — audiences and contacts live in D1. Manage them from the Audience view; export them whenever you like.
- Double opt-in — signups land as
pendingand only become subscribers when the person confirms by email. Only confirmed contacts are ever sent to, so an import can't quietly start mailing people who never asked. - Embeddable signup widget — drop
<script src=".../widget.js">on your own site; it starts the same opt-in flow. - Sending — send now, or send a test to yourself. Bring your own API key.
- Email-safe rendering — table-wrapped, inline-styled HTML with a
per-subscriber unsubscribe footer, plus the
List-Unsubscribeheaders mailbox providers expect from bulk senders.
flowchart TD
prompt(["Prompt"]) --> draft["AI draft · OpenRouter"]
draft --> mail[("Mail · D1")]
mail --> renderer["renderer"]
tokens["DESIGN.md tokens"] --> renderer
renderer --> html["email-safe HTML"]
contacts[("Contacts · D1")] --> send["one message per subscriber"]
html --> send
send --> provider["your sending provider"]
One message per subscriber rather than a single broadcast, so each carries its own unsubscribe link — a shared link would let whoever clicks it unsubscribe everyone.
A template = a DESIGN.md token set + a content skeleton. Each mail can
override the template's tokens; the design panel edits that override live and
"Save as…" serializes it back to the DESIGN.md format.
| Layer | Technology |
|---|---|
| Frontend | React, TypeScript, Tailwind CSS v4, Vite, shadcn/ui |
| Backend | Hono (Cloudflare Worker) |
| Database | D1 (mails, templates, settings, audiences, contacts) |
| Bring your own provider — Resend wired today | |
| AI | OpenRouter (configurable model) |
| Icons | Lucide |
git clone https://github.com/clawnify/open-newsletter.git
cd open-newsletter
pnpm install
pnpm dev # Vite on :5173, Worker + local D1 on :8787Open http://localhost:5173. The schema is applied to local D1 automatically.
To exercise sending and generation locally, copy the example env file:
cp .dev.vars.example .dev.varsRESEND_API_KEY=re_xxxxxxxx # https://resend.com/api-keys
OPENROUTER_API_KEY=sk-or-xxxxxxxx # https://openrouter.ai/keys
# NEWSLETTER_MODEL=anthropic/claude-sonnet-4 (optional override)
Restart pnpm dev after editing .dev.vars.
In Settings, set your publication name and a from address on a domain you've verified with your sending provider. Sending is refused from an unverified domain, and you'll be told which one and where to fix it.
Audiences and contacts are yours — create an audience in the Audience view; no provider dashboard involved.
Point people at the embeddable widget, or POST /api/subscribe from your own
form:
<div data-newsletter-subscribe></div>
<script src="https://<your-app>/widget.js"></script>Either way it's double opt-in: the contact is created pending and only
becomes a subscriber once they click the confirmation link. Sends go to
confirmed subscribers only.
Adding someone by hand from the Audience view also lands them pending. If
you're migrating a list that already has recorded consent, pass
consent_evidence to POST /api/audiences/:id/contacts to record how it was
obtained and mark them subscribed.
npx clawnify deployclawnify.json declares the env contract:
| Env | Required | Purpose |
|---|---|---|
RESEND_API_KEY |
for sending | Your own key — delivery only; contacts stay in D1 |
OPENROUTER_API_KEY |
for AI | The Generate button |
NEWSLETTER_MODEL |
no | Override the generation model |
On Clawnify these are injected automatically from your org's API keys / environment variables at deploy time — no secrets in the app.
src/
shared/
design.ts — DESIGN.md token model, panel metadata, CSS-var + serializer
templates.ts — built-in templates (runtime mirror of templates/*/DESIGN.md)
markdown.ts — email-safe Markdown → HTML
types.ts — Mail, Template, Settings, Contact types
server/
index.ts — Hono API (mails, templates, settings, audiences,
subscribe/confirm/unsubscribe, widget, generate, send)
contacts.ts — audiences + contacts + the consent lifecycle
render.ts — mail + tokens → email-safe inlined HTML
ai.ts — OpenRouter generation
providers/ — EmailProvider interface (send-only) + adapters
schema.sql — D1 schema
client/
app.tsx — shell + nav
store.tsx — app state
components/
editor.tsx — top bar, preview/edit, generation bar
preview.tsx — live canvas (mirrors render.ts)
design-panel.tsx — the DESIGN.md token editor
… — mails, templates, audience, settings views
DESIGN.md — the default brand (Classic Editorial), Google Labs format
templates/<slug>/ — each template as DESIGN.md + content.md
- HTML-to-image components — author rich illustrations, diagrams, and "component" graphics in HTML + CSS and render them to static images at send time, so custom visuals stay email-safe (email clients don't run JS or modern CSS). Makes branded illustrations and charts a drop-in block.
- AI sources — ground a draft in real data instead of just a prompt. Connect a source and the generator pulls from it. First up: GitHub commits → generate a "what we shipped this week" issue straight from your repo history. Planned: changelogs, product analytics, RSS/Atom, Linear/Jira.
MIT
