BuilderJS

Reusable Blocks and Saved Rows — Stored in Your Database, Not a Vendor's

"Saved rows" and reusable blocks are one of the most-requested features in any embeddable editor: let an end user design a row once — a hero, a footer, a product card — name it, and drop it into the next email or page without rebuilding. Hosted builders sell this as a managed feature, and they meter it. Beefree's SDK, for example, lists "hosted saved rows" as a usage-based line item layered on top of its monthly plans (as of June 2026, per Beefree's pricing and usage-based-fees docs). With BuilderJS the economics flip: you own the primitives, you store the saved rows in your own database, and you serve unlimited tenants with zero per-row vendor fees. The trade is honesty about scope — BuilderJS does NOT ship a hosted "Saved Rows" panel as a turnkey UI. What it ships is the two real, documented primitives you compose that feature from: `Builder.registerElement` for permanent custom blocks baked into the palette, and a `getData()` JSON-subtree capture + `load()` re-insert pattern for user-saved rows. This guide explains both, where the line is, and exactly what you build versus what the library gives you — so you can ship reusable blocks that live on your servers, branded as yours, multi-tenant from day one.

Mount it

// USER-SAVED ROWS — the real pattern. No hosted panel; you store the JSON.
  // A "row" is one block subtree from getData(): { name, template, formats, elements:[...] }
  
  // 1) SAVE a row the user picked, into YOUR database (scoped to the tenant).
  async function saveRow(builder, tenantId, label, blockIndex) {
    const data = builder.getData();          // design JSON; data.blocks is the array
    const row  = data.blocks[blockIndex];    // one block + its elements = the row
    await fetch('/api/saved-rows', {         // YOUR backend, YOUR DB — nothing phones home
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ tenant_id: tenantId, label, json: row }),
    });
  }
  
  // 2) RE-INSERT a stored row back into the current design, then reload.
  async function insertSavedRow(builder, themeTemplates, themeConfigData, mediaUrl, savedRowId) {
    const { json: savedRow } = await (await fetch(`/api/saved-rows/${savedRowId}`)).json();
    const data = builder.getData();
    data.blocks.unshift(savedRow);           // or splice at any index
    // load() reassigns templates wholesale — pass themeTemplates every time.
    builder.load(data, themeTemplates, themeConfigData, mediaUrl);
  }
  
  // PERMANENT custom block for everyone (different primitive): register once,
  // BEFORE any load(). The registry is consulted at load time.
  Builder.registerElement('BrandBadgeElement', BrandBadgeElement);
  builder.templates.BrandBadge = BRAND_BADGE_TEMPLATE;
  themeTemplates.BrandBadge    = BRAND_BADGE_TEMPLATE; // survives every load()

Two different problems: permanent blocks vs. user-saved rows

"Reusable blocks" actually covers two distinct needs, and BuilderJS solves them with two different primitives. The first is the permanent, developer-defined block: a Countdown element, a brand badge, a styled CTA that should always be in the palette for every user, every tenant. That is an extension to the engine — you author it once and register it with `Builder.registerElement(name, klass)`. The second is the user-saved row: a section a specific end user assembled in the canvas and wants to keep — their footer, their signature block, their pricing layout — and re-drop later. That is not an engine extension; it is just data. A row in BuilderJS is a subtree of the design JSON, so "save this row" means capturing that subtree from `getData()` and storing it wherever you keep user data. The mistake is conflating them: registerElement is for things you ship to everyone; the JSON-capture pattern is for things individual users create. Get the split right and the rest is straightforward.

Permanent custom blocks with registerElement

For blocks every user should always have, BuilderJS exposes a real, documented extension API: `Builder.registerElement(name, klass)`. You subclass the engine's `BaseElement` (implementing `static parse(data)`, `getData()`, and `_doRender()`), provide an EJS template, and register the class — one time, before any `builder.load()` call. Registration mutates the engine's element registry and exposes the class on `window`, so JSON nodes carrying that element name parse and render through the exact same path stock elements use. The shipped demos under `demo/examples/extensions/` (a CountdownElement, a BrandBadgeElement with a custom color control) are working references you copy. Two non-negotiables from the docs: the registry is consulted at load time, so register before you load; and because `load()` reassigns the templates object wholesale, your custom template must also live on the `themeTemplates` object you pass into every `load()`. This is how you bake permanent, branded, reusable blocks into the palette — owned in your source, no fork required.

User-saved rows: capture a getData() subtree, store it as your data

For rows your end users save themselves, there is no special API to learn — it is plain JSON. `builder.getData()` returns the whole design as an object whose `blocks` array holds every block, where each block carries its `name`, `template`, `formats`, and an `elements` array. A block (plus its elements) IS a row. To let a user "save this row," capture that one block object from `getData().blocks`, serialize it, and write it to your database alongside the user/tenant id, a name, maybe a thumbnail. Nothing leaves your servers. To re-place it later, read the stored JSON, splice it back into the current design's blocks array — `data.blocks.unshift(savedBlock)` (or insert at any index) — and call `builder.load(data, themeTemplates, themeConfigData, mediaUrl)`. The engine rehydrates the block exactly, including any field values your controls set. The shipped `3-custom-control` demo does precisely this round-trip. Because the unit of storage is a portable JSON subtree, you can scope it per user, per team, or per tenant in your own schema — a multi-tenant saved-rows library that costs you database rows, not vendor metering.

What you build vs. what the library gives you (be honest here)

This is the line to state plainly: BuilderJS does NOT ship a hosted "Saved Rows" side-panel, a managed library UI, or a backend that stores rows for you. There is no turnkey panel to toggle on. What the library gives you are the load-bearing primitives — `registerElement` for permanent blocks, `getData()`/`load()` for capturing and re-placing row subtrees — plus reference PHP backends for save and upload that you copy and own. The panel itself (a list of a user's saved rows, a Save-as-row button, drag-to-insert, thumbnails, sharing across a team) is application UI you build around those primitives, against your own database. That is more work than flipping a SaaS feature flag — but it is the reason there is no per-row fee, no MAU meter, no vendor holding your users' content. You design the storage model, the permissions, and the panel; BuilderJS guarantees the design data is clean JSON you fully control on both the capture and restore sides.

Why own the primitives: multi-tenant, no metering, your data

The pull is structural. A hosted saved-rows feature is a recurring cost that scales with your success: more tenants and more saved content mean more metered usage. Because BuilderJS is a one-time CodeCanyon license (Regular $52 / Extended $99) with no SaaS tier, no per-seat charge, no usage metering, and no phone-home, the saved rows your users create are just rows in your database — free to multiply across unlimited tenants. Multi-tenancy is supported at the architecture level (the editor instance is closure-captured, so isolated instances can coexist), while your app owns the actual isolation, naming, and access rules — which is exactly what you want when one tenant's saved rows must never leak to another. You also avoid the classic lock-in trap: the design is already JSON in your store and the HTML is a function you call, so there is no vendor between you and your users' content. You build the convenience layer once; you keep the data forever. BuilderJS is the editor you embed — it builds the markup; persistence, sharing, and the saved-rows UX are yours to own.

Frequently asked questions

Explore