Wire a real backend
Save · load · upload · auth · multi-tenant — 60 minutes round-trip.
The shipped demo/backend/save.php writes builder JSON to disk under demo/uploads/. That's fine for the demo. For production you'll swap it for your own stack — MySQL / Postgres / S3 / Firebase — plus auth and tenant-scoping. This doc walks the shipped contract and four adapter patterns. Time budget: 60 minutes.
The save / load contract
When the buyer clicks Save, the engine POSTs to whatever URL you configured as saveUrl:
Your handler returns JSON:
The shipped demo/backend/save.php handler
Read it (60 LOC). It's the buyer's reference for a minimum-viable backend:
- Validator pipeline — chain
required(),matches(),maxBytes(),isJson()calls. The library isdemo/backend/_lib/Validator.php— copy it into your project. - Path traversal guard —
refuses('sample', '/\.\./')blocks../etc/passwdstyle attacks. Apply to every user-controlled path. - Atomic-ish write —
mkdirparent +file_put_contents. For real DBs use a transaction; for S3 use a singlePutObject(it's atomic).
Adapter — MySQL
Replace the file write with a query. The shipped examples/backend/1-mysql/ walks this end-to-end. The skeleton:
Adapter — S3 / object storage
For static-site or serverless deployments, write the HTML + JSON to object storage:
Auth — wrap the handler
Every save is authenticated. Wrap your handler with whatever your app uses:
Multi-tenant — closure capture
The W6.D lesson: the engine stays tenant-naive. Tenant identity lives in the host's Builder constructor closure, not on the engine instance.
Asset upload
The engine never opens a file input or POSTs an upload itself. Instead it
calls your onBrowse(handleUrl) callback when the buyer clicks Browse on any
image-bearing control. The host opens its own picker (modal / native / 3rd
party), uploads to its own endpoint, then calls handleUrl(url) with the
resulting public URL:
Async hand-off for heavy exports
The W6.C.15 lesson: HTML / ZIP / Bundle exports can be slow on big pages. Don't block the buyer's UI on a 30-second export. Pattern:
- POST
/api/v1/exports/queuewith the page id. - Server enqueues a background job, returns
{ "jobId": "abc-123" }. - Buyer's UI shows a "exporting…" toast.
- Server-Sent Events (SSE) or polling against
/api/v1/exports/status?jobId=abc-123returns progress. - On completion, the response includes a download URL.
This pattern matters for large galleries, multi-language exports, and any case where post-processing exceeds 2-3 seconds.
Did you make it?
You should now be able to:
- ✅ State the save contract: 4 form fields + JSON status response.
- ✅ Adapt
save.phpto MySQL with apagestable and user/tenant scoping. - ✅ Adapt
save.phpto S3 / object storage with aPutObjectcall. - ✅ Add an auth wrapper (session OR JWT) to any backend handler.
- ✅ Use closure-capture for multi-tenant Builder instances.
If a step didn't work:
- 🩹 Save returns 422 → your validator caught a malformed field. Check the response's
errorsarray — each entry names which field failed. - 🩹 Save returns 500 → tail your PHP error log. Most often: DB connection failed (wrong host/creds), filesystem write failed (permissions), or a fatal in your handler.
- 🩹 Builder doesn't fire the Save POST → confirm
saveUrlis set in the constructor options. If not set, clicking Save throwssaveUrl is not configured. Pass it in the constructor options. - 🩹 Multi-tenant accidentally crosses tenants → the closure isn't capturing the right
tenantId. Re-read the closure — everynew Builder(...)call MUST have its own server-rendered constants.
What's next?
- → i18n locale packs — multi-language locale support, the production polish for SaaS builds.
- 🔗
examples/backend/1-mysql/— runnable MySQL adapter end-to-end. - 🔗
examples/backend/2-s3/— S3 + MinIO. - 🔗
examples/backend/3-auth/— session + JWT. - 🔗
examples/backend/4-realtime/— WebSocket multi-window sync. - 🔗
examples/extensions/5-multi-tenant/— closure-capture pattern.