Skip to content
MIT-licensed editor & renderer Works with any ESPRuns at the edge

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

11 built in

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.

14 ESP syntaxes

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.

13 clients

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.

WCAG 2.1 AA

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.

~70 tokens

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

MIT

The 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

MIT

Renders 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.

01

Build in the visual editor

Drag, drop, and nest rows and columns into a layout — no code required.

02

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."

03

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.

04

Publish

The SDK picks up the change on its next TTL cycle automatically — no redeploy required.

Cache resolution sequence

default TTL: 5 min · configurable
Memory cache

In-process heap. Best for long-lived servers — Express, NestJS, FastAPI.

0ms once warm
miss
Disk cache

Filesystem-persisted. Opt in for serverless — survives cold starts on Lambda, Cloud Run.

cold-start safe
miss
Maildeno server

Template JSON fetch — happens at most once per template per TTL window.

1×/TTL max
always
Render engine

Local Wasm/native binary. Runs in-process, every render, on your own server.

private

If 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

AWS SESPostmarkResendMailgunSendGridSparkPostBrevoAny SMTP

Any runtime

ExpressNestJSNext.jsFastAPIDjangoLambda
HIPAA / GDPR friendly

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.

  • 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

    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

    Not supported

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.