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
-
Is BuilderJS a multi-tenant SaaS platform I can resell as-is?
No. BuilderJS is the embeddable editor, and it is multi-tenant capable because each instance keeps its own closure-captured configuration. Your application owns tenant isolation, seats, billing, and access control — the engine does not manage accounts or metering. You embed it inside the SaaS you already operate.
-
How do I isolate one tenant's data from another?
Closure-capture a tenantId per Builder so saves and uploads route to tenant-scoped URLs, then enforce it server-side. Your save and upload handlers validate the tenantId, re-check the authenticated user is allowed that tenant, and write under a tenant-scoped directory or storage prefix. The engine never sees your customer database; it only POSTs to the URL your closure supplied.
-
Does each tenant need a separate license or pay per seat?
No. BuilderJS is a one-time CodeCanyon license (Regular $52 / Extended $99) with no per-seat charge, usage meter, or revenue share. You own the source and self-host it. If your end users are charged, the Extended tier is the one to choose; always review the current CodeCanyon terms for your specific distribution model before launch.
-
Can different tenants see different themes or templates?
Yes, but the filtering is your responsibility, not the engine's. Your backend resolves the list of themes a tenant is permitted to use and hands each editor only that filtered set when you call load(). The engine never sees the unfiltered catalog — it simply loads whatever theme data your host code provides for that tenant.
-
Why doesn't the engine just take a tenantId option directly?
Tenant identity is a host concern: the engine does not know what a tenant is or how to authenticate one, and baking that in would push scope, lifecycle, and auth decisions into a component that should not own them. It would also add hook code that single-tenant apps would ship for nothing. Closure capture in your code gives complete isolation while keeping the engine bundle identical for every deployment.