How to save BuilderJS content to a database
BuilderJS is the embeddable editor — it builds the markup and hands you a lossless JSON document, but it never persists anything for you. Saving to a database is deliberately your application's job, and it takes two real moving parts: a `saveUrl` you pass to the constructor, and `builder.save()`, which POSTs the rendered HTML plus the JSON state to that URL. There is no SaaS, no phone-home, and no engine `save:click` event — you wire your own button to `save()`, and your backend writes the row. This guide walks the exact, source-accurate contract: what the constructor expects, what `save()` actually sends on the wire, how the shipped reference MySQL handler stores it, and how to reload a saved design later with `load()`. Everything below uses the real BuilderJS API — no invented methods.
Mount it
// 1) Mount the builder with a saveUrl pointing at YOUR endpoint.
// saveUrl is a plain string — there is no save:{url,csrf} object.
const builder = new Builder({
mainContainer: '#MainContainer',
widgetsContainer: '#WidgetsContainer',
settingsContainer: '#SettingsContainer',
saveUrl: '/examples/backend/1-mysql/mysql-save.php',
upload: { url: '/backend/asset-upload.php', maxBytes: 5_000_000 },
loadAssets: 'auto',
});
// 2a) Simplest path: wire your own button to builder.save().
// save() POSTs { html, data } (url-encoded) and resolves with the
// server's JSON. There is NO save:click event — this is host-driven.
document.querySelector('#saveBtn').addEventListener('click', async () => {
try {
const res = await builder.save(); // POST html + data -> saveUrl
console.log('Saved:', res); // server's parsed JSON response
} catch (err) {
console.error('Save failed:', err); // surface to your own UI
}
});
// 2b) Keyed/CSRF path: the reference mysql-save.php also wants a `slug`,
// which builder.save() does NOT add. Hand-roll the fetch instead,
// reading the real getHtml()/getData() outputs yourself.
async function saveKeyed(slug, csrf) {
const body = new URLSearchParams();
body.append('slug', slug);
body.append('html', builder.getHtml());
body.append('data', JSON.stringify(builder.getData()));
const res = await fetch('/examples/backend/1-mysql/mysql-save.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRF-Token': csrf },
body: body.toString(),
});
return res.json();
}
// 3) Round-trip: load the stored JSON back later (lossless restore).
// builder.load(theme, templates, configData, mediaUrl) — 4 args.
const saved = await (await fetch('/api/pages/my-page')).json();
builder.load(saved.theme, saved.templates, saved.configData, saved.mediaUrl);
Configure saveUrl, then call save()
Persistence starts with one constructor option: `saveUrl`, a string pointing at your own endpoint. (It is a plain string — there is no `save:{url,csrf}` object; pass `saveUrl` directly.) Once set, calling `builder.save()` returns a Promise. Internally it runs `getHtml()` and `getData()`, then issues a `fetch` POST with `Content-Type: application/x-www-form-urlencoded` containing exactly two fields: `html` (the rendered output) and `data` (the JSON state, `JSON.stringify`-ed). The Promise resolves with `response.json()`. There is no `save:click` event on the engine — save is host-driven, so you wire any button, keyboard chord, or debounced autosave to `save()` yourself. If `saveUrl` is unset, `save()` throws.
What lands on the wire
Be precise about the payload so your handler validates the right shape. `builder.save()` sends a URL-encoded body with `html` and `data` only — no slug, no CSRF token, no auth header. If your endpoint needs a record key (to upsert a specific page) or a CSRF token, the built-in `save()` won't add them. In that case, skip `save()` and hand-roll the fetch: read `builder.getHtml()` and `builder.getData()` yourself, append your own `slug`/`csrf`/headers, and POST to your route. Both approaches are first-class; `save()` is the convenience path for the simplest contract, and the manual fetch is the path when your schema is keyed or your framework requires a token.
The reference MySQL handler
The download ships a reference PHP backend at `examples/backend/1-mysql/mysql-save.php` you copy and own. It accepts a URL-encoded POST of `slug`, `html`, and `data`, validates each (slug is regex-bounded against path traversal; html and data have byte caps that fire before PDO), confirms `data` is real JSON, then runs a single prepared UPSERT into a `pages` table — `INSERT … ON DUPLICATE KEY UPDATE` on MySQL 8, `INSERT … ON CONFLICT(slug) DO UPDATE` on SQLite/PostgreSQL. It returns JSON `{ status, slug, message }`. Because it is just a reference talking to PDO, the same contract ports to Node, Go, or Python in a few lines. Note it expects a `slug`, so wire it with the hand-rolled fetch pattern, not bare `save()`.
Round-trip: reload with load()
Saving is only half the loop — you also need to restore the exact canvas. On the next visit, read the stored JSON back from your database and pass it into `builder.load(themeJson, themeTemplatesJson, themeConfigData, mediaUrl)`. The `getData()` ↔ `load()` round-trip is lossless: a saved document reopens byte-for-byte, including all element types, controls, and styles. Because the format is plain JSON you own outright, you can version it, diff it across edits, migrate the schema, or render it server-side — there is no proprietary save format and nothing locks you in. Your app keeps full control of routing, auth, multi-tenant isolation, and where the bytes live.
Frequently asked questions
-
How does BuilderJS save content to a database?
It doesn't write to the database directly — it hands your backend the data and your backend writes the row. Set the `saveUrl` constructor option to your endpoint, then call `builder.save()`. That POSTs the rendered HTML and the JSON state (from getHtml() and getData()) as a URL-encoded body to your URL; your handler validates it and runs the INSERT/UPSERT. BuilderJS never persists anything itself and never phones home.
-
What exactly does builder.save() send?
A single POST with Content-Type application/x-www-form-urlencoded and two fields: `html` (the rendered output of getHtml()) and `data` (JSON.stringify of getData()). It returns a Promise that resolves with the server's parsed JSON response. It does not add a slug, CSRF token, or auth header — if you need those, read getHtml()/getData() and POST with your own fetch instead.
-
Is there a save button or save:click event I can hook?
No. There is no save:click event on the engine and no built-in save button. Save is host-driven by design: you place your own button (anywhere in your UI) and call builder.save() on click, or trigger it from a keyboard shortcut or a debounced autosave timer. The engine only owns the editing surface; when to persist is your decision.
-
How do I reload a saved design?
Fetch the JSON you stored back from your database, then call builder.load(themeJson, themeTemplatesJson, themeConfigData, mediaUrl). The getData()/load() round-trip is lossless, so the canvas restores exactly as it was saved. Since the data is plain JSON you own, you can also version, diff, or migrate it freely.
-
Does BuilderJS host the page or send the email after I save it?
No. BuilderJS is the editor you embed — it produces the page HTML or Outlook-safe email HTML and the JSON model, and that is where its job ends. It does not host or publish pages, send email, run funnels, or process payments. After you save the markup, your own hosting/CMS serves the page and your own ESP/SMTP sends the email.