BuilderJS

How to Add Version History to Your Embedded Email Builder

Version history — the "saved 3 minutes ago / restore this revision" panel you see in Gmail's compose history, Google Docs, or Beefree — is one of those features users assume is built in, then quietly resent paying extra for. In most hosted builders, revision history sits behind a higher subscription tier, and the revisions themselves live in the vendor's database, not yours. With BuilderJS the situation flips: you already own the one primitive the whole feature is built on, and you already own the data. BuilderJS is the embeddable, self-hosted drag-and-drop editor for HTML email and pages — it builds the markup and hands you a lossless JSON document, but it never persists, hosts, or sends anything for you. That boundary is exactly why version history is yours to build cleanly. The engine guarantees one thing that matters here: a lossless round-trip, where load(getData(x)) reconstructs the design x exactly. Everything else — capturing each revision, listing them, diffing two snapshots, and restoring one — is a small amount of code on top of your own database, using methods you already have. This guide walks the honest, source-accurate pattern: which BuilderJS calls do the work, what you have to build yourself, and where the engine's responsibility ends so you wire your capture to the right signal instead of a callback the engine never fires for you.

Mount it

// Capture a revision on save, restore it later, diff two snapshots.
  // BuilderJS guarantees ONLY the lossless getData() <-> load() round-trip.
  // save() does NOT fire any 'saved' event — capture on its Promise instead.
  // The revisions table, list UI, diff, and restore button are YOURS.
  
  const builder = new window.Builder({
    mainContainer: '#bjs-canvas',
    widgetsContainer: '#bjs-widgets',
    settingsContainer: '#bjs-settings',
    saveUrl: '/api/templates/42/save', // your endpoint
  });
  
  // --- CAPTURE: hook the save() Promise; there is no engine 'saved' event.
  async function saveAndSnapshot() {
    // Option A: built-in save() POSTs html+data, resolves with your JSON response.
    await builder.save();
    // (capture the revision here, off the resolved Promise — not an event)
  
    // Option B (keyed/CSRF schema): hand-roll instead of save().
    const data = builder.getData();
    await fetch('/api/templates/42/revisions', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ data, label: 'autosave' }), // INSERT into template_revisions
    });
  }
  
  // --- RESTORE: read a stored revision's JSON, feed it back through load().
  async function restoreRevision(revisionId) {
    const { data } = await fetch(`/api/revisions/${revisionId}`).then(r => r.json());
    // Optional: snapshot current state first so restore is itself undoable.
    builder.load(data, themeTemplatesJson, themeConfigData, mediaUrl); // lossless
  }
  
  // --- DIFF: revisions are plain JSON, so compare them however you like.
  function diffRevisions(a, b) {
    return { /* your JSON-diff: walk element tree, compare control values */ };
  }

The one primitive that makes version history trivial: a lossless round-trip

Every revision system is, underneath, a list of snapshots you can restore. BuilderJS gives you the snapshot and the restore as two plain methods. `builder.getData()` returns a complete, serializable JSON representation of the current design — every element type, control value, and style. `builder.load(themeJson, themeTemplatesJson, themeConfigData, mediaUrl)` rebuilds the canvas from that JSON, passed back as the first argument. The contract that makes this safe for version history is that the round-trip is lossless: feed a saved `getData()` document back through `load()` and you reopen the exact design, not a flattened approximation. That is the difference between a real revision ("restore exactly what I had at 2pm") and a lossy re-parse. Because the snapshot is just JSON you hold, a revision is nothing more than one `getData()` result stored with a timestamp. You are not reverse-engineering rendered HTML — which is hard and lossy — you are versioning the source-of-truth design state directly. That single guarantee is what the rest of this feature stands on.

Capturing a revision: hook the save() Promise, not an engine event

Be precise here, because it changes your wiring. Saving in BuilderJS is host-driven. You get a `saveUrl` constructor option and a `builder.save()` method: calling `save()` runs `getHtml()` and `getData()` internally, POSTs `html` and `data` (URL-encoded) to your `saveUrl`, and returns a Promise that resolves with your server's JSON response. Crucially, `save()` itself does not emit any "saved" event — it just POSTs and resolves — so your reliable "a save just happened" signal is the resolution of that Promise, in the `.then(...)`, where you push a revision row. BuilderJS does expose a `DOCUMENT_SAVED` constant in its frozen `Builder.EVENTS` enum, but treat it as a host-emitted convention, not an engine callback: the engine never fires it for you on `save()`, so you'd have to emit it yourself after a successful save — which is more wiring than simply acting on the Promise. (There is no `save:click` event at all.) The clean pattern: wire your own Save button — or a debounced autosave, or a keyboard chord — to `save()`, and capture the revision when the Promise resolves. If your schema needs a record key or CSRF token, which the built-in `save()` does not send, skip `save()` and hand-roll the fetch: read `builder.getData()` yourself, append your `slug`/`csrf`/headers, POST to your route, and write the revision in the same handler. Either way, the capture point is your code, fired off your save. (Separately, the engine emits an in-editor `history:change` event, but that is the session-local undo/redo stack — it lives in memory and resets on reload, so it is not a cross-session revision log. Don't confuse the two.)

Push each snapshot to a revisions table you own

With the capture point in your hands, persistence is ordinary database work. Keep your current design in its usual `pages` (or `templates`) row, and add a `template_revisions` table: an id, the template id it belongs to, the `getData()` JSON, a created-at timestamp, and whatever metadata you want — author id, a label, the rendered HTML if you also want a preview. On every successful save, insert a new revision row. That is the whole storage layer. Because the JSON is yours and there is no proprietary save format, you control retention entirely: keep the last N revisions per template, prune by age, or snapshot only on explicit "Save version" rather than every autosave — your call, your data, no vendor quota. This is the pull most hosted builders charge for: they keep revisions in their infrastructure and gate access behind a plan tier. Here the revisions sit in your database next to everything else you store, queryable and exportable, with no per-revision metering and nothing phoning home. The engine never sees this table; it only ever hands you JSON and consumes JSON.

Restore a revision with load() — and diff snapshots because they're just JSON

Restoring is the round-trip in reverse. Read the chosen revision's stored JSON from your database and pass it straight into `builder.load(themeJson, themeTemplatesJson, themeConfigData, mediaUrl)` as the first argument. The canvas rebuilds to exactly that point in time — losslessly, by the same guarantee that made the snapshot trustworthy. A good UX is to insert a fresh revision capturing the current state before you restore, so "restore" is itself undoable; that's a one-line insert in your code, not an engine concern. Diffing is equally yours and equally easy precisely because a revision is plain, structured JSON, not opaque HTML. You can compare two snapshots with any JSON-diff approach — walk the element tree, compare control values, or run a structural diff library — and render the result however your product wants: a side-by-side, a "changed blocks" summary, or a simple field-level list. None of that touches BuilderJS; you're operating on data you stored. The same portability means you can migrate the revision schema, render an old snapshot server-side for a thumbnail, or export a version elsewhere — there is no lock-in around the format.

Where the line sits: the engine guarantees the round-trip, you own the feature

State this plainly so expectations stay honest: BuilderJS gives you exactly one guarantee for version history — the lossless `getData()` ↔ `load()` round-trip, plus the `save()` Promise as a convenient capture point. The version-list panel, the timestamps and labels, the "restore" and "compare" buttons, the diff view, the retention policy, and the revisions table are all yours to build and own. That is not a gap; it is the model. BuilderJS is the editor you embed and self-host, not a platform — it does not store revisions, run a history service, send the resulting email, or host the resulting page. You bring the database, the UI, and the sending or hosting path. The payoff is that a feature competitors meter and lock to their cloud becomes a modest amount of code over data you already control, with the one hard part — losslessly capturing and restoring a rich design — handled by primitives the engine already exposes. BuilderJS ships under a one-time CodeCanyon license (Regular $52 / Extended $99) with full source you self-host: no SaaS, no monthly fee, no per-seat or per-revision charge. You can try the round-trip live at builder.emotsy.com before you build on it.

Frequently asked questions

Explore