The email template layer for engineering teams.
Design in a visual editor, attach merge-tag and visibility logic, then export clean HTML, MJML, or React Email. Render it on your own server — or inside a Cloudflare Worker — and send with whatever ESP you already use. No lock-in. No migration cost. No scattered template files.
- Editor & renderer
- MITEditor & renderer
- Runtime dependencies
- 0Runtime dependencies
- Export formats
- 4Export formats
- ESP syntaxes
- 14ESP syntaxes
- Clients previewed
- 13Clients previewed
One template
Any output
<table role="presentation">
<tr><td>
<h1>Welcome, {{ name }}</h1>
</td></tr>
</table>What's in the box
Everything the hard part of email needs.
Drag-and-drop is the easy half. The rest is table-based HTML that survives Word's rendering engine, MSO fallbacks, four dark-mode behaviours, a size ceiling, and fourteen ESP dialects that cannot all express the same condition. That is what these packages are.
For the person building the email
Eleven block types, plus your own
Paragraph, heading, image, video, list, button, anchor, divider, spacer, menu and socials. Multi-column rows nest five deep, with per-block mobile overrides.
Merge tags that escape correctly
Tags are group-qualified as text, url or attr, and each group is escaped for where it lands. A URL in the wrong group is the single most common personalisation bug — the grouping makes it impossible.
Visibility rules, resolved either way
Show a row only when plan is premium. Resolve it before sending and ship a smaller email, or emit your ESP's own conditional syntax and let them evaluate it.
Thirteen email clients modelled
Apple Mail, Gmail, Outlook, Yahoo, ProtonMail and more — including four genuinely different dark-mode behaviours. The preview simulates what each client actually does, rather than making everything look decisive.
Accessibility checking, built in
A WCAG 2.1 AA-aligned rule catalogue with critical and serious severities, merge-tag-aware text checks, and per-issue suppression that survives a reload.
Size and spam-risk indicators
Live size estimation against Gmail's ~102 KB clipping threshold, plus a text-to-image ratio. Shown as a range, because a precise number nobody can justify is worse than an honest estimate.
For the person embedding it
Storage stays yours
A seventeen-method adapter where every method is optional — anything you leave out falls back to localStorage. Four methods is a real integration. The editor never sees a tenant id, so isolation is your session's job, not the browser's.
Themed to your product
Around seventy colour tokens covering surfaces, chrome, selection and status, with full dark-mode variants. Re-themes live, no remount — wire it to your existing theme switcher and it moves in the same frame.
Version history and saved rows
Restore, keep and prune versions through five optional adapter methods — each control appears only when you implement it. Plus a personal row library and a read-only shared one for org-wide blocks.
A drawer for your assistant
The editor ships the place an AI goes, not the AI. You get the drawer, its trigger, focus handling and a small canvas API. Your model, your prompt, your data policy, your bill.
Four export formats from one document — html, mjml, react-email and the portable json you should actually store. Full feature reference →
Maildeno Open Source
Open source, by default.
The editor and renderer are MIT-licensed and fully functional with zero Maildeno backend. Connect Maildeno Cloud only if you want cross-device sync, team collaboration, or the image CDN — and that is a one-line adapter change, in either direction.
@maildeno/editor
You design
template.json
Your database
@maildeno/renderer
You render
No API key. No network call. Maildeno Cloud is one optional branch, not a requirement.
@maildeno/editor
MITThe same drag-and-drop editor used in the dashboard, as an embeddable component. A native Vue component, or a standard custom element that drops into any framework — no Vue knowledge required of the caller, and no wrapper package to fall out of date.
- License
- MIT
- Backend
- Not required
- Frameworks
- Any — ships as a custom element
npm install @maildeno/editor
import { useEffect, useRef } from "react";
import { init, type EditorHandle } from "@maildeno/editor/init";
export default function Editor() {
const container = useRef<HTMLDivElement>(null);
const handle = useRef<EditorHandle | null>(null);
useEffect(() => {
let cancelled = false;
init({
container: container.current!,
onSave: ({ templateId }) => save(templateId, handle!.getJson())
})
return () => {
cancelled = true;
handle.current?.destroy();
};
}, []);
return <div ref={container} />;
}@maildeno/renderer
MITRenders a template JSON file to HTML, MJML or React Email entirely offline, through the same engine that powers Maildeno Cloud. No API key, no account, no network call — in Node, at the edge, or in the browser.
- License
- MIT
- Dependencies
- Zero
- Runtimes
- Node, edge, browser
npm install @maildeno/renderer
import { render } from "@maildeno/renderer"
const html = await render("templates/welcome.json", {
mergeTags: {
text: { first_name: "Noruwa" }, // HTML-escaped
url: { cta: "https://app.example.com" }, // URL-encoded
},
context: { plan: "premium" }, // drives visibility rules
})Runs where you run
One import. Every runtime.
The renderer is a Rust engine compiled to WebAssembly, embedded in the package. The same import works unchanged in Node, in a browser, in a Cloudflare Worker, in Vercel Edge and in Deno — and the output is byte-identical everywhere, because it is the same compiled engine everywhere.
0
Runtime dependencies
Not “few” — zero. Nothing transitive to audit.
0
Network calls
No code path opens a socket. Nothing to firewall.
~9 KB
Node bundle
The engine is a separate Wasm file, read from disk.
~90 KB
Brotli, at the edge
Engine inlined. Rarely the constraint on a Worker.
Wrangler picks the edge build automatically. Nothing to configure.
import { render } from "@maildeno/renderer"
export default {
async fetch(request, env) {
// KV's "json" type parses for you
const template = await env.TEMPLATES.get("welcome", "json")
const html = await render(template, {
mergeTags: { text: { first_name: "Noruwa" } },
})
return new Response(html, {
headers: { "content-type": "text/html" },
})
},
}The only difference between runtimes
Node can take a file path. Everywhere else there is no file system, so you read the template however suits that runtime — a KV or R2 binding, fetch(), a bundler JSON import — and pass the parsed object in. Passing a path throws immediately with a message telling you exactly that, rather than failing somewhere deeper with fs is not defined.
Every option, every output format and every error code behaves identically. The right build is selected by package.json export conditions, so there is nothing to configure.
Maildeno Cloud
The hosted platform, end to end.
Design in the dashboard, render through typed SDKs with built-in caching and reliability — and ship with whichever ESP and runtime you already run.
How product teams and developers work together
One workflow, two roles — neither waits on the other.
Build in the visual editor
Drag, drop, and nest rows and columns into a layout — no code required.
Define merge tags & visibility rules
Tag placeholders as text, URL, or attribute; attach show/hide conditions by context — "show this row only when plan = pro."
Debug with the visual logic debugger
Drop a context object into the editor and see exactly what renders — no deploy, no console log, no guessing.
Publish
The SDK picks up the change on its next TTL cycle automatically — no redeploy required.
Cache resolution sequence
default TTL: 5 min · configurableIn-process heap. Best for long-lived servers — Express, NestJS, FastAPI.
0ms once warmFilesystem-persisted. Opt in for serverless — survives cold starts on Lambda, Cloud Run.
cold-start safeTemplate JSON fetch — happens at most once per template per TTL window.
1×/TTL maxLocal Wasm/native binary. Runs in-process, every render, on your own server.
privateIf the server is unreachable — serve stale from cache
Renders from the last known-good copy in memory or disk and sets fromStaleCache: true. It only throws on a cold miss — unreachable and no prior cache entry at all.
Works with what you already run
Any ESP
Any runtime
Zero PII egress
Merge tags and visibility context are applied by the render engine on your own server. Maildeno's servers see the template — never your customer data.
npm install maildeno
import { MaildenoClient } from "maildeno"
const client = new MaildenoClient({ apiKey: process.env.MAILDENO_API_KEY })
const html = await client.renderHtml(templateId, {
merge_tags: { text: { name: "Noruwa" } },
})Honest comparison
Why not just use what you have?
Often you should. Here is where each of the usual four approaches is genuinely the right answer, and the specific point at which each one starts to hurt.
Hosted embeddable builder
A drag-and-drop editor licensed per seat or per project, backed by the vendor's servers.
Good at:Polish, template galleries, a support contract.
Hurts when:Your customers' template data lives at a third party, pricing scales with seats, and the vendor is now in your compliance review.
Code-first frameworks
Write email in a better syntax and compile it to HTML.
Good at:Developer ergonomics, version control, a mature ecosystem.
Hurts when:A non-engineer needs to change the copy. Every edit becomes a pull request.
Your ESP's own editor
The builder inside your marketing platform.
Good at:It is already there, already wired to their data.
Hurts when:You switch ESPs. The export button gives you HTML — a build artifact, not a source you can re-render.
Building it in-house
Your team writes the editor and the export pipeline.
Good at:Fits your product exactly.
Hurts when:Month three, when the drag-and-drop works and Outlook still does not. The export pipeline is the part nobody scopes correctly.
What Maildeno does and does not do
The last two rows are as load-bearing as the first eight.
- Supported
Templates stored in your own database
Yes — via an adapter you write
- Supported
Non-engineers can edit without a deploy
Yes — visual editor
- Supported
Works with any ESP
Yes — Maildeno never sends
- Supported
Renders with no network call
Yes — local Wasm engine
- Supported
Runs at the edge
Yes — Workers, Vercel, Deno
- Supported
Source you can read and fork
Yes — MIT, both packages
- Supported
Keeps working if the vendor disappears
Yes — no server in the path
- Supported
Mobile editing
Yes — desktop and mobile
- Not supported
Sends the email
No — that is your ESP's job
- Not supported
Writes the copy for you
No — a drawer for your AI, not an AI
Questions
The things people actually ask.
Including the ones with an unflattering answer.
No. Maildeno produces the email and hands it to whatever you already send with — SES, Postmark, Resend, Mailgun, SendGrid, or your own SMTP. That is deliberate: sending is a solved problem with real switching costs, and adding another provider to your stack is not what you came here for.
Something not covered? Read the docs or open an issue.
Stop scattering templates across every ESP.
Free tier available. No credit card required. Your customer data never leaves your server.
Or skip the sign-up entirely — npm install @maildeno/editor works with no account at all.