BuilderJS

How to Embed a Multi-Tenant Page Builder in Your SaaS

BuilderJS is multi-tenant capable, but it is honest to be precise about what that means: it is the embeddable drag-and-drop editor your users design in, not a tenancy platform. The engine itself stays tenant-naive — there is no tenant constructor option, no this.tenantId, and no tenant event on the bus. Instead, you give each tenant their own isolated editor by closure-capturing a tenantId in the Builder constructor's saveUrl and onBrowse handlers. Two tenants means two closures, which means two different save and asset-upload destinations from one shared engine bundle. Your SaaS keeps full ownership of the parts the editor should never touch: tenant isolation, seats, billing, auth, and where designs are stored. This guide shows the verified pattern from the multi-tenant reference example so you can scope an editor per tenant without forking the source.

Mount it

// Multi-tenant SaaS: closure-capture tenantId per Builder instance.
  // The engine stays tenant-naive — there is NO `tenant` constructor
  // option. tenantId lives only in the host's closures (saveUrl + onBrowse).
  
  function buildEditorFor(tenantId) {
    // tenantId is closure-captured; the engine never sees it.
    const builder = new window.Builder({
      mainContainer:     '#' + tenantId + '-canvas',     // CSS selector string
      widgetsContainer:  '#' + tenantId + '-widgets',    // CSS selector string
      settingsContainer: '#' + tenantId + '-settings',   // CSS selector string
  
      // Per-tenant save URL — closure-captured tenantId in the query string.
      // The engine POSTs html + data here on save; your backend reads
      // tenantId, validates it, and routes to the tenant's save directory.
      saveUrl: '/backend/save.php?tenantId=' + encodeURIComponent(tenantId),
  
      // Per-tenant asset browse — the engine calls onBrowse(handleUrl) when
      // the user clicks Browse. The host opens its own picker and attaches
      // tenantId via extraFields so uploads land in a tenant-scoped root.
      onBrowse: function (handleUrl) {
        window.DemoFileBrowser.pickFile(
          function (r) { handleUrl(r.url); },
          {
            endpoint:    '/backend/asset-upload.php',
            extraFields: { tenantId: tenantId },   // <- closure-captured
          }
        );
      },
    });
  
    // Hand THIS tenant only the theme data it is permitted to load.
    // Your backend resolves the filtered set; the engine just loads it.
    // load(themeJson, themeTemplatesJson, themeConfigData, mediaUrl) — 4 args
    builder.load(window.THEME_JSON, window.THEME_TEMPLATES, window.THEME_CONFIG_DATA, window.MEDIA_URL);
  
    return builder;
  }
  
  const editorA = buildEditorFor('acme-corp');
  const editorB = buildEditorFor('globex-llc');
  
  // editorA.options.saveUrl -> '/backend/save.php?tenantId=acme-corp'
  // editorB.options.saveUrl -> '/backend/save.php?tenantId=globex-llc'
  // One engine bundle. Two complete tenant isolations.
  // There is no destroy(); on unmount the host discards the container DOM.

Multi-tenant is a host concern, not an engine concern

BuilderJS deliberately keeps tenancy out of the engine. There is no tenant constructor option, no tenantId field on the instance, and no tenant-scoped event. The engine only knows how to render the canvas, hand you JSON via getData(), restore it via load(), POST to a saveUrl, and call your onBrowse when a user picks an asset. Tenant identity — what a tenant is, who is authenticated, what they may access — belongs to your application, which already owns auth and a database. The benefit is that single-tenant deployments ship the same lean bundle, and you never inherit a vendor's seat or session accounting. You embed a finished editor and decide independently how it is scoped and metered.

Closure-capture tenantId in saveUrl and onBrowse

The pattern is small and durable: a factory function takes a tenantId and returns a new Builder whose saveUrl and onBrowse close over it. The tenantId is baked into those handlers for the lifetime of that editor. The save endpoint receives the tenant in its query string; the asset picker attaches it via extraFields so uploads land in a tenant-scoped storage root. Build editorA with one tenantId and editorB with another, and you get two complete isolations from one loaded engine. Per-instance state — history, recent colors, active controls — is already isolated per Builder, so two editors coexist cleanly without leaking into each other.

The server validates and routes per tenant

Closure capture sets the destination, but the server enforces it. Your save handler reads tenantId from the request, validates its shape, and re-checks that the authenticated user is actually allowed that tenant — defense in depth, not trust in the client string. A small registry then answers what the tenant may access: the directory their saved pages write to, the storage prefix their uploads land under, and the filtered list of themes they are permitted to load. In production this comes from your tenant store — a database query, LDAP lookup, or feature-flag service. BuilderJS ships reference PHP backends for save, upload, and auth as copy-and-own starter code; you adapt them to your tenancy model or port them to Node, Go, or Python.

What BuilderJS still leaves to your SaaS

Be clear on the boundary, because that is what makes the model trustworthy. BuilderJS builds the template and the markup; it does not manage accounts, subscriptions, seat limits, or access control, and it never reaches into your customer database. It also does not send email, host or publish pages, run funnels, or process payments — those are downstream of export and belong to your stack. For email it produces the markup through an export pipeline; for pages it emits standalone HTML. You wire your existing tenant guard in front of every save and upload route, store each tenant's JSON scoped behind your own auth, and keep billing where it already lives. The editor hands you portable JSON; your platform decides who can read and write it.

Frequently asked questions

Explore