Every client wants something slightly different, and AI just made those differences cheap to build. I think that changes how products get shaped, so I built a sandbox for it: sandbox-environment, Apache-2.0, zero runtime dependencies, on JSR as @panth977/sandbox.
I'm Panth Patel. I work on backends and developer tooling, and this is the argument behind the library more than a tour of it.
From product to framework
Many companies went from solutions to SaaS: stop building one thing per client and sell one thing to everyone. My view is that the next move runs from product to framework. With AI around, and with less organised sectors coming online, each client will have different needs and will expect them met.
Those needs have to live somewhere. The easy answer is to have AI stitch them into your app. It works for a while. Then your product code holds every client's special case, and each customisation is a product change with a product release.
The better principle is a line. Framework code on one side, client customisation on the other, and a sandbox as that line. Yes, you'll probably still use AI to write what goes inside it. That is fine. What changes is where the code lands.
It gives you two things. A customisation that needs no product code change can ship in hours instead of weeks. And if the client has a tech team of their own, you can hand them the sandbox and let them maintain it.
What it looks like
A customisation is an artifact: a mini-app mounted in a slot of your app.
import { mount, scopes, confirm, audit } from "jsr:@panth977/sandbox/host";
mount(document.getElementById("slot")!, {
artifact,
api: {
devices: {
list: (q) => http.get("/devices", { params: q }), // uses YOUR token
},
},
middleware: [
audit((e) => log.push(e)),
scopes({ "devices.list": "devices:read" }, granted),
],
});
Inside the artifact, the code only sees the API you passed in:
import { host } from "@panth977/sandbox/artifact";
const rows = await host.devices.list();
Your frontend is its backend. The call crosses a typed bridge, runs in your app with the viewer's permissions, and comes back. Streams, callbacks, cancellation and errors cross it too.
Where the boundary comes from
I didn't write a security model. I used the browser's. Each artifact runs in an <iframe sandbox="allow-scripts"> with an opaque origin, so it has no access to your DOM, cookies, localStorage or tokens. A strict CSP closes the network unless the artifact's manifest asks and the host allows it. Scopes, confirm-before-destructive, rate limits, an audit trail and dry runs are middleware, a line each.
The threat model is written down in SECURITY.md. The artifact is untrusted, and the host and its API are trusted. Two things are out of scope: an artifact wasting its own frame, and anything your API chooses to allow. The sandbox stops the code from reaching around your API. You still have to design that API with care.
Writing artifacts in the browser
A sandbox nobody can write for doesn't help. So the package includes the tools:
- A compiler that takes TypeScript, JSX and Svelte in the browser or a worker, with no bundler and no WASM. The same compiler runs in Node for CI.
-
mountDev: a Monaco editor with completions and go-to-definition, plus a live preview. - A UI kit, so artifacts look like part of your app.
Hosts can be plain JS, Svelte or Angular, tested in CI in Chromium, Firefox and WebKit, with Angular as far back as 15. Artifacts can be plain JS, React, Preact or Svelte 5. A React host should work through the custom element, but I haven't tested one in a real React app yet.
What's next
This release covers the frontend. The next one is a backend sandbox: write the code in any language, while the host keeps full control over the script and its security.
Where do client customisations live in your product today?
Top comments (0)