Skip to main content

Cloudflare Gave Every AI-Generated App Its Own SQLite Database — Here's What That Costs to Replicate on Machines You Own

10 min readDora NodaDora Noda
Share
On this page

AI agents no longer just write code snippets. They generate entire applications — with a UI, with long-lived state, with a database. And that last part is the one nobody had a good answer for: when an agent spins up one app per end user, where does each app's data live, and what keeps tenant A's rows out of tenant B's hands?

Cloudflare's answer arrived during Agents Week in April 2026: Durable Object Facets. A platform-written supervisor object loads agent-generated code on the fly and runs it as a child facet — and every facet gets its own isolated SQLite database on local disk. Per-tenant data isolation stops being a pattern you build and audit, and becomes a runtime primitive you get for free. This post explains what Facets actually does, why database-per-tenant beats shared-database-plus-RLS for generated apps, and what the same guarantee concretely costs to replicate on machines you own.

What Facets actually is: a supervisor object plus a database per child

The mechanism is easiest to see in the shape Cloudflare itself documented. You write one normal Durable Object class — call it AppRunner — and each instance of it manages exactly one generated application. It stores the app's code, loads it through the Dynamic Worker Loader API, and executes it as a named facet:

ts
// Inside AppRunner, a platform-written Durable Object.
// Each instance manages one AI-generated app.
let facet = this.ctx.facets.get("app", async () => {
  // #loadDynamicWorker fetches the stored code via the Loader API (elided).
  let worker = this.#loadDynamicWorker(); // agent-generated code
  let appClass = worker.getDurableObjectClass("App");
  return { class: appClass };
});
return await facet.fetch(request);

Three details make this more than a naming trick. First, each facet gets its own SQLite database, isolated from the parent's: the generated app can read and write only its own database, never the supervisor's. One AppRunner instance is therefore two databases — parent plus child — with a hard boundary between them. Second, a single object can host any number of facets (subject to storage limits), so the supervisor pattern scales from one app per object to many. Third, the sandbox is genuinely locked down: the documented example passes globalOutbound: null, blocking all network access from the untrusted generated code.

The surrounding numbers matter too. Dynamic Workers run on isolates, not containers, which Cloudflare pegs at roughly 100x faster load with a tenth of the memory — the reason "spin up a sandbox per app" is cheap enough to be a default rather than a luxury. Each Durable Object carries up to 10 GB of SQLite storage, and Facets shipped in beta on the Workers Paid plan. Agents Week also delivered the demand side of the same story: Code Mode, where agents write TypeScript instead of making individual tool calls, reportedly cutting token usage by 81%. Cheaper generation means more generated apps, which means the per-app database question stops being theoretical fast.

Why per-tenant SQLite beats shared-database-plus-RLS here

Multi-tenant data isolation has three classic shapes, and generated apps stress them unevenly:

ModelHow isolation worksFailure domainAudit burden
Shared DB, shared schema + RLSEvery row carries a tenant id; Postgres Row Level Security policies filter accessOne database: a policy bug or a missed WHERE clause leaks across tenantsHigh — every policy, migration, and query path must be reviewed
Shared DB, schema per tenantOne database, one schema namespace per tenantBetter blast radius, but still one running database to operate and back upMedium — schema provisioning and search-path hygiene per tenant
Database per tenant (Facets)Each tenant app gets its own SQLite file; there is no shared table to leak acrossOne tenant's corruption, eviction, or traffic spike touches only its own databaseLow — isolation holds by construction, even for code you never reviewed

That last row is the whole argument. When a human team writes the app, auditing RLS policies is tractable. When an agent generates a new app per user, the code under the policy is different every time — and "we reviewed every generated query" is a promise no platform team can keep. Facets inverts the responsibility: isolation is a property of the runtime (separate database files, separate storage), not a property of the generated code. The untrusted app cannot exfiltrate a neighbor's rows because the neighbor's rows live in a database it has no handle to.

There is a performance side to the same choice. A Durable Object's SQLite database lives on local disk on the machine where the object runs, so reads avoid a network hop to a separate database tier entirely — effectively zero-latency storage access. And SQLite's file-per-database model means cold tenants cost almost nothing: an idle generated app is a file on disk, not a connection slot, not a provisioned Postgres role, not a row in a shared table competing for buffer cache.

What the same guarantee costs on machines you own

Facets is an edge-runtime primitive, not a portable library. Replicating "one isolated SQLite instance per tenant app" on a Cluster-API-managed fleet over owned hardware means picking where the per-tenant boundary lives. There are three serious options, in increasing order of fidelity to the Facets shape:

OptionWhat each tenant getsIsolation strengthOperational price
A. Postgres per tenant (CloudNativePG)A dedicated Postgres database — or cluster — provisioned per tenant app via the operatorStrong: separate database, separate credentials, Postgres-grade durabilityHighest: per-tenant Postgres processes eat memory idle, and N tenants means N backup, upgrade, and connection-pooling surfaces to operate
B. SQLite per pod + object-store replicationEach tenant pod mounts its own SQLite file, continuously replicated to S3-compatible storage with Litestream (or LiteFS for read replicas)Strong at rest and at runtime: separate files, separate volumes; single-writer semantics per tenant, which matches one-app-per-tenant anywayMedium: you operate the replication sidecar and the restore path, but idle tenants are just objects in a bucket — the closest cost curve to Facets
C. celld (self-hosted Durable Objects)A "cell": a named server with its own SQLite database, running the Workers/Durable Objects JavaScript APIs on your own machinesSame model as Facets by design — per-object SQLite, API-compatible with the Cloudflare surfaceLowest marginal cost per tenant, but youngest ecosystem: Ryan Dahl's open-source runtime (Deno, August 2026) trades Cloudflare's global placement for hardware you control

Option A is the conservative enterprise answer and the most expensive per tenant: a Postgres instance that exists to serve one small generated app still wants its memory footprint, its backups, and its major-version upgrades. It makes sense when tenants need Postgres-specific features (rich types, extensions, concurrent writers) rather than just "a private place to put rows."

Option B is the shape closest to what Facets actually is — files, not servers. SQLite's single-writer model, often cited as a limitation, is a non-issue when each database serves exactly one tenant app; write contention across tenants cannot happen because there is no shared database to contend over. Litestream-style replication to S3-compatible storage gives you the durability story (point-in-time restore from the object store) without running a database server per tenant at all. The honest gap versus Cloudflare is placement and spin-up: your fleet provisions pods in regions you operate, not in 300+ edge locations, and a cold pod starts in seconds, not in isolate milliseconds.

Option C is the newest and most direct: celld, which Dahl's team calls "a love letter" to Durable Objects, reimplements Workers, Durable Objects, KV, Queues, D1, and R2 as a self-hosted daemon backed by S3-compatible storage on the Tokio runtime. The pitch is explicit — "orders of magnitude cheaper at scale" than metered Durable Objects — and the API compatibility means code written against the Facets model ports over. The tradeoff is maturity: an August 2026 open-source runtime does not yet have Cloudflare's decade of edge operations behind it, so adopters are buying into a roadmap, not just a binary.

None of the three reproduces the full Cloudflare package — global anycast placement plus millisecond isolate boot plus ten-gigabyte objects is a property of Cloudflare's network, not of any software you can install. But all three reproduce the guarantee that matters: isolation by construction for code you did not write and cannot fully audit.

Where owning the machine wins anyway

The comparison above concedes the edge advantages honestly. Now the other direction, because Facets' constraints are real and they all point at ownership:

Data residency and audit access. A facet's SQLite file lives on Cloudflare's disk, visible only through Cloudflare's APIs. On your own fleet, the tenant database is a file on a volume you control: you can snapshot it, SELECT against a frozen copy during an incident, ship it to a regulator, or prove exactly which bytes left the building. For generated apps handling customer data, "the platform vendor holds the only copy of the storage layer" is a compliance conversation; "here is the volume snapshot" ends it.

No metering at scale. Facets rides the Workers pricing model: requests, duration, and storage, metered per unit. That model is friendly at small scale and punishing at the exact scale a successful per-user-app platform reaches — thousands of tenants, each generating steady background traffic. Owned hardware inverts the curve: the marginal cost of one more tenant database trends toward the cost of disk, and Dahl's "orders of magnitude cheaper" claim for celld is, at its core, just this arithmetic stated bluntly.

No platform ceiling. Facets today means beta status, a Paid plan gate, and 10 GB per object. Those are reasonable beta constraints, and they are also someone else's roadmap to relax. A fleet you operate sets its own ceilings: bigger volumes, longer retention, custom backup windows, no feature flag between you and the primitive.

Debuggability with real primitives. When a generated app misbehaves on the edge, you get logs and dashboards. When it misbehaves on your machine, you get the process, the file descriptor, the SQLite file itself — strace, a volume snapshot, a query plan against production-shaped data. For AI-generated code, which fails in ways its author (an agent, unavailable for questions) never anticipated, that depth of inspection is not a luxury.

The primitive is the point

Facets' lasting contribution is not the API — ctx.facets.get() will evolve — it is the normalization of a new default: every generated app gets its own database, and isolation is the runtime's job, not the generated code's. That default survives the port off Cloudflare's network. Whether you implement it with a Postgres operator, a folder of Litestream-replicated SQLite files, or a self-hosted object runtime like celld, the architecture converges on the same shape: supervisor loads untrusted code, untrusted code gets a database it cannot see past.

The teams that internalize that shape early — before their agent story graduates from "writes snippets" to "ships stateful apps per user" — will skip the painful middle phase of bolting tenant isolation onto generated code after the fact. The database-per-tenant decision is cheapest on day one, when the tenant count is small and every option above is still easy to adopt.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Star the repo on GitHub or deploy your first app today.

Related articles

Give your agents a chain backend

Autonomous agents hit RPC endpoints very differently than people do. See what bex router handles on their behalf.

Read the agents guide