Durable Objects in Dynamic Workers: geef elke door AI gegenereerde app een eigen database.
Dit artikel is ook beschikbaar in English, Deutsch, Español, Español (Latinoamérica), Français, Italiano, 日本語, 한국어, 繁體中文 en 简体中文.

Enkele weken geleden kondigden we Dynamic Workers aan, een nieuwe functie van het Workers-platform waarmee je Worker-code dynamisch in een beveiligde sandbox kunt laden. De Dynamic Worker Loader-API biedt kort gezegd directe toegang tot het fundamentele principe van rekenisolatie waarop Workers altijd al gebaseerd is geweest: isolates, niet containers. Isolates zijn veel lichter dan containers en kunnen daardoor 100 keer sneller laden met slechts 1/10 van het geheugen. Ze zijn zo efficiënt dat je ze als 'wegwerpbaar' kunt beschouwen: start er een op om een paar regels code uit te voeren en gooi hem daarna weer weg. Een soort beveiligde versie van eval().
Dynamic Workers kennen veel toepassingen. In de oorspronkelijke aankondiging lag de nadruk op hoe je ze kunt gebruiken om door AI-agents gegenereerde code uit te voeren als alternatief voor tool calls. In dit gebruiksscenario voert een AI-agent acties uit op verzoek van de gebruiker door een paar regels code te schrijven en deze uit te voeren. De code is voor eenmalig gebruik, bedoeld om één taak eenmalig uit te voeren en wordt direct na uitvoering verwijderd.
Maar wat als je wilt dat een AI-code genereert die langer meegaat? Wat als je wilt dat je AI een kleine applicatie bouwt met een aangepaste gebruikersinterface waarmee de gebruiker kan communiceren? Wat als je wilt dat die applicatie een langdurige staat behoudt? Maar natuurlijk wil je nog steeds dat het in een veilige sandbox draait.
Een manier om dit te doen is door gebruik te maken van Dynamic Workers en de Worker simpelweg een RPC-API te geven die toegang tot de opslag mogelijk maakt. Met behulp van bindings kun je de Dynamic Worker een API geven die verwijst naar je externe SQL-database (bijvoorbeeld een database die wordt ondersteund door Cloudflare D1 of een PostgreSQL-database die je bereikt via Hyperdrive – dat bepaal je zelf).
Maar Workers beschikt ook over een uniek en extreem snel type opslag dat perfect kan passen bij dit gebruiksscenario: Durable Objects. Een Durable Object is een speciaal soort Worker met een unieke naam, waarvan er wereldwijd slechts één instantie per naam bestaat. Aan die instantie is een SQLite-database gekoppeld, die op de lokale schijf van de machine draait waarop het Durable Object actief is. Hierdoor is de toegang tot de opslag razendsnel: er is praktisch geen latentie.
Misschien wil je dan eigenlijk dat je AI-code schrijft voor een Durable Object en dat je die code vervolgens uitvoert in een Dynamic Worker.
Maar hoe dan?
Dit levert een vreemd probleem op. Normaal gesproken moet je, om Durable Objects te gebruiken, het volgende doen:
- Schrijf een klasse die
DurableObjectuitbreidt. - Exporteer het vanuit de hoofdmodule van je Worker.
- Geef in je Wrangler-configuratie aan dat er opslagruimte moet worden ingericht voor deze klasse. Dit creëert een Durable Object-namespace die verwijst naar je klasse voor het afhandelen van inkomende verzoeken.
- Declareer een namespacebinding voor een Durable Object die naar je namespace verwijst (of gebruik ctx.exports) en gebruik deze om verzoeken naar je Durable Object te sturen.
Dit is niet vanzelfsprekend van toepassing op Dynamic Workers. Ten eerste is er het voor de hand liggende probleem: de code is dynamisch. Je draait het zonder de Cloudflare-API aan te roepen. Maar Durable Object-opslag moet via de API worden ingericht en de namespace moet verwijzen naar een implementerende klasse. Het kan niet naar je Dynamic Worker verwijzen.
Er is echter een dieperliggend probleem: zelfs als je op de een of andere manier een Durable Object-namespace zo zou kunnen configureren dat deze rechtstreeks naar een Dynamic Worker verwijst, zou je dat dan wel willen? Wil je dat je agent (of gebruiker) een complete namespace vol Durable Objects kan aanmaken? Wil je onbeperkte opslagruimte over de hele wereld gebruiken?
Waarschijnlijk niet. Je wilt waarschijnlijk een vorm van controle hebben. Wellicht wil je het aantal objecten dat ze aanmaken beperken of in ieder geval bijhouden. Misschien wil je ze beperken tot slechts één object (waarschijnlijk voldoende voor vibe-coded persoonlijke apps). Je wilt waarschijnlijk ook logging en andere vormen van 'observability' toevoegen. Statistieken. Facturering. Enz.
Om dit allemaal te kunnen, wil je eigenlijk dat verzoeken naar deze Durable Objects eerst door jouw code gaan, waar je alle 'logistiek' kunt afhandelen, en dat verzoeken daarna worden doorgestuurd naar de code van de agent. Je wilt een soort supervisor schrijven die onderdeel is van elk Durable Object.
Oplossing: Durable Object Facets
Vandaag introduceren we in open bèta een functie die dit probleem oplost.

Met Durable Object Facets kun je dynamisch een Durable Object-klasse laden en instantiëren en deze voorzien van een SQLite-database voor opslag. Met facets:
- Eerst maak je een normale Durable Object-namespace aan, die verwijst naar een klasse die je zelf schrijft.
- In die klasse laad je de code van de agent als een Dynamic Worker en roep je deze aan.
- De code van de Dynamic Worker kan rechtstreeks een Durable Object-klasse implementeren. Dat wil zeggen: het exporteert letterlijk een klasse die is gedeclareerd als
extends DurableObject. - Je instantieert die klasse als een 'facet' van je eigen Durable Object.
- De facet krijgt een eigen SQLite-database, die via de normale Durable Object Storage-API's kan worden gebruikt. Deze database staat los van de database van de supervisor, maar beide worden samen opgeslagen als onderdeel van hetzelfde Durable Object.
Hoe het werkt
Nu volgt een eenvoudige, complete implementatie van een app-platform dat dynamisch een Durable Object-klasse laadt en uitvoert:
import { DurableObject } from "cloudflare:workers";
// For the purpose of this example, we'll use this static
// application code, but in the real world this might be generated
// by AI (or even, perhaps, a human user).
const AGENT_CODE = `
import { DurableObject } from "cloudflare:workers";
// Simple app that remembers how many times it has been invoked
// and returns it.
export class App extends DurableObject {
fetch(request) {
// We use storage.kv here for simplicity, but storage.sql is
// also available. Both are backed by SQLite.
let counter = this.ctx.storage.kv.get("counter") || 0;
++counter;
this.ctx.storage.kv.put("counter", counter);
return new Response("You've made " + counter + " requests.\\n");
}
}
`;
// AppRunner is a Durable Object you write that is responsible for
// dynamically loading applications and delivering requests to them.
// Each instance of AppRunner contains a different app.
export class AppRunner extends DurableObject {
async fetch(request) {
// We've received an HTTP request, which we want to forward into
// the app.
// The app itself runs as a child facet named "app". One Durable
// Object can have any number of facets (subject to storage limits)
// with different names, but in this case we have only one. Call
// this.ctx.facets.get() to get a stub pointing to it.
let facet = this.ctx.facets.get("app", async () => {
// If this callback is called, it means the facet hasn't
// started yet (or has hibernated). In this callback, we can
// tell the system what code we want it to load.
// Load the Dynamic Worker.
let worker = this.#loadDynamicWorker();
// Get the exported class we're interested in.
let appClass = worker.getDurableObjectClass("App");
return { class: appClass };
});
// Forward request to the facet.
// (Alternatively, you could call RPC methods here.)
return await facet.fetch(request);
}
// RPC method that a client can call to set the dynamic code
// for this app.
setCode(code) {
// Store the code in the AppRunner's SQLite storage.
// Each unique code must have a unique ID to pass to the
// Dynamic Worker Loader API, so we generate one randomly.
this.ctx.storage.kv.put("codeId", crypto.randomUUID());
this.ctx.storage.kv.put("code", code);
}
#loadDynamicWorker() {
// Use the Dynamic Worker Loader API like normal. Use get()
// rather than load() since we may load the same Worker many
// times.
let codeId = this.ctx.storage.kv.get("codeId");
return this.env.LOADER.get(codeId, async () => {
// This Worker hasn't been loaded yet. Load its code from
// our own storage.
let code = this.ctx.storage.kv.get("code");
return {
compatibilityDate: "2026-04-01",
mainModule: "worker.js",
modules: { "worker.js": code },
globalOutbound: null, // block network access
}
});
}
}
// This is a simple Workers HTTP handler that uses AppRunner.
export default {
async fetch(req, env, ctx) {
// Get the instance of AppRunner named "my-app".
// (Each name has exactly one Durable Object instance in the
// world.)
let obj = ctx.exports.AppRunner.getByName("my-app");
// Initialize it with code. (In a real use case, you'd only
// want to call this once, not on every request.)
await obj.setCode(AGENT_CODE);
// Forward the request to it.
return await obj.fetch(req);
}
}
In dit voorbeeld:
AppRunneris een 'normaal' Durable Object, geschreven door de platformontwikkelaar (jij).- Elke instantie van
AppRunnerbeheert één applicatie. Het slaat de appcode op en laadt deze op verzoek. - De applicatie zelf implementeert en exporteert een Durable Object-klasse, waarvan het platform verwacht dat deze de naam
Appheeft. AppRunnerlaadt de applicatiecode met behulp van Dynamic Workers en voert de code vervolgens uit als een Durable Object Facet.- Elke instantie van
AppRunneris een Durable Object dat bestaat uit twee SQLite-databases: één die toebehoort aan de parent (AppRunnerzelf) en één die toebehoort aan de facet (App). Deze databases zijn geïsoleerd: de applicatie kan de database vanAppRunnerniet lezen, alleen zijn eigen database.
Om het voorbeeld uit te voeren, kopieer de bovenstaande code naar een worker.js-bestand, combineer het vervolgens met het volgende wrangler.jsonc-bestand, en voer het lokaal uit met npx wrangler dev.
// wrangler.jsonc for the above sample worker.
{
"compatibility_date": "2026-04-01",
"main": "worker.js",
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": [
"AppRunner"
]
}
],
"worker_loaders": [
{
"binding": "LOADER",
},
],
}
Begin met bouwen
Facets zijn een functie van Dynamic Workers, die direct beschikbaar zijn in bèta voor gebruikers met een betaald Workers-abonnement.
Raadpleeg de documentatie voor meer informatie over Dynamic Workers en facets.