Durable Object Facets を使うと、Dynamic Worker から Durable Object クラスを読み込み、自分の Durable Object の子として実行できます。子(Facet)は独自の分離された SQLite データベースを持ち、自分のクラスはアクセスを制御するスーパーバイザーとして動きます。
動的に生成したコード(たとえば AI エージェントが書いたコード)に永続ストレージを持たせたい一方で、Durable Object 名前空間への直接アクセスは渡したくない場合に向いています。スーパーバイザーがコードを読み込み、Facet を作成し、リクエストを転送します。動的コードができることの制御は、自分側に残ります。
Facet を使う構成は 3 層です。
- スーパーバイザークラス — 自分で書いてデプロイする、通常の Durable Object クラスです。ほかの Durable Object と同じく、SQLite ストレージバックエンドを設定します。
- 動的コード — Worker Loader API 経由で実行時に読み込むコードです。このコードは
DurableObjectを継承するクラスをエクスポートします。 - Facet — スーパーバイザー内で
this.ctx.facets.get()を呼んで作る、動的クラスのインスタンスです。各 Facet は、スーパーバイザーとは別の独自 SQLite データベースを持ちます。
スーパーバイザーのデータベースと Facet のデータベースは、同じ Durable Object 全体の一部としてまとめて保存されます。動的コードはスーパーバイザーのデータベースを読めません。アクセスできるのは自分のデータベースだけです。
Worker に必要なものは 2 つです。SQLite ストレージバックエンドを持つ Durable Object クラスと、Worker Loader バインディングです。
{
"$schema": "./node_modules/wrangler/config-schema.json",
// Set this to today's date
"compatibility_date": "2026-09-20",
"main": "src/index.ts",
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": [
"AppRunner"
]
}
],
"worker_loaders": [
{
"binding": "LOADER"
}
]
}# Set this to today's date
compatibility_date = "2026-09-20"
main = "src/index.ts"
[[migrations]]
tag = "v1"
new_sqlite_classes = ["AppRunner"]
[[worker_loaders]]
binding = "LOADER"次の例では、スーパーバイザー Durable Object(AppRunner)が動的コードを読み込み、そこから Facet を作成し、HTTP リクエストを Facet へ転送します。
動的コードは、受け取ったリクエスト数を独自の SQLite バックエンドストレージで数える、シンプルなカウンターアプリです。本番では、このコードは静的な文字列ではなく、AI エージェントやユーザーのアップロードから来ます。
import { DurableObject } from "cloudflare:workers";
// In production, this code would come from an AI agent, a database,
// or user input — not a static string.
const AGENT_CODE = `
import { DurableObject } from "cloudflare:workers";
export class App extends DurableObject {
fetch(request) {
// Note: storage.kv provides simple KV storage backed by SQLite,
// but you can also use SQL directly via storage.sql. See:
// https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/
let counter = this.ctx.storage.kv.get("counter") || 0;
++counter;
this.ctx.storage.kv.put("counter", counter);
return new Response("You have made " + counter + " requests.\\n");
}
}
`;
// AppRunner is your supervisor. Each instance manages one
// dynamically-loaded application.
export class AppRunner extends DurableObject {
async fetch(request) {
// Get a stub pointing to the "app" facet. If the facet has not
// started yet (or has hibernated), the callback runs to tell the
// runtime what code to load.
const facet = this.ctx.facets.get("app", async () => {
const worker = this.#loadDynamicWorker();
// Extract the Durable Object class named "App" from the
// dynamic Worker's exports.
const appClass = worker.getDurableObjectClass("App");
return { class: appClass };
});
// Forward the request to the facet.
// You can also call RPC methods on the stub.
return await facet.fetch(request);
}
#loadDynamicWorker() {
// Use get() so the Worker stays warm across requests.
// Each unique code version needs a unique ID.
const codeId = "agent-code-v1";
return this.env.LOADER.get(codeId, async () => {
return {
compatibilityDate: "2026-04-01",
mainModule: "worker.js",
modules: { "worker.js": AGENT_CODE },
globalOutbound: null, // block network access
};
});
}
}
export default {
async fetch(request, env, ctx) {
// Look up the AppRunner instance named "my-app".
const obj = ctx.exports.AppRunner.getByName("my-app");
// Forward the request to it.
return await obj.fetch(request);
},
};import { DurableObject } from "cloudflare:workers";
// In production, this code would come from an AI agent, a database,
// or user input — not a static string.
const AGENT_CODE = `
import { DurableObject } from "cloudflare:workers";
export class App extends DurableObject {
fetch(request) {
// Note: storage.kv provides simple KV storage backed by SQLite,
// but you can also use SQL directly via storage.sql. See:
// https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/
let counter = this.ctx.storage.kv.get("counter") || 0;
++counter;
this.ctx.storage.kv.put("counter", counter);
return new Response("You have made " + counter + " requests.\\n");
}
}
`;
// AppRunner is your supervisor. Each instance manages one
// dynamically-loaded application.
export class AppRunner extends DurableObject<Env> {
async fetch(request: Request): Promise<Response> {
// Get a stub pointing to the "app" facet. If the facet has not
// started yet (or has hibernated), the callback runs to tell the
// runtime what code to load.
const facet = this.ctx.facets.get("app", async () => {
const worker = this.#loadDynamicWorker();
// Extract the Durable Object class named "App" from the
// dynamic Worker's exports.
const appClass = worker.getDurableObjectClass("App");
return { class: appClass };
});
// Forward the request to the facet.
// You can also call RPC methods on the stub.
return await facet.fetch(request);
}
#loadDynamicWorker() {
// Use get() so the Worker stays warm across requests.
// Each unique code version needs a unique ID.
const codeId = "agent-code-v1";
return this.env.LOADER.get(codeId, async () => {
return {
compatibilityDate: "2026-04-01",
mainModule: "worker.js",
modules: { "worker.js": AGENT_CODE },
globalOutbound: null, // block network access
};
});
}
}
export default {
async fetch(
request: Request,
env: Env,
ctx: ExecutionContext,
): Promise<Response> {
// Look up the AppRunner instance named "my-app".
const obj = ctx.exports.AppRunner.getByName("my-app");
// Forward the request to it.
return await obj.fetch(request);
},
};この例では、次のとおりです。
AppRunnerはスーパーバイザー Durable Object です。通常どおりデプロイし、Durable Object 名前空間を所有します。- 動的コードは
DurableObjectを継承するクラス(App)をエクスポートします。このクラスは、ほかの Durable Object と同じくthis.ctx.storageでデータを読み書きします。 this.ctx.facets.get("app", callback)が Facet を作成します。"app"という文字列が Facet の名前です。名前ごとに、親 Durable Object 内で独自の SQLite データベースを持ちます。- Facet のデータベースは、スーパーバイザーのデータベースから完全に分離されています。
AppRunnerとAppは、相手がアクセスできない独自のストレージを持ちます。
this.ctx.facets オブジェクトは、任意の Durable Object クラス内で使えます。Facet の作成、シャットダウン、削除のメソッドを提供します。1 つの Durable Object に、名前の異なる Facet をいくつでも持てます。それぞれが独立した SQLite データベースを持ちます。
this.ctx.facets.get(name string, callback () => FacetStartupOptions) Fetcher
指定した名前の Facet を作成または再開し、リクエストを送るためのスタブを返します。
Facet がまだ起動していない、またはハイバネーションしている場合、ランタイムは getStartupOptions を呼んで読み込むコードを決めます。それ以外では既存の Facet を再利用し、コールバックは呼ばれません。callback は任意で async にできます(つまり Promise<FacetStartupOptions> を返す)。
返されるスタブは Durable Object スタブ と同じように動きます。.fetch() で HTTP リクエストを送ることも、RPC メソッドを直接呼ぶこともできます。
this.ctx.facets.abort(name string, reason any) void
稼働中の Facet をシャットダウンし、既存のスタブをすべて無効にします。無効になったスタブへの以降の呼び出しは reason をスローします。Facet のストレージは残ります。
abort したあと、get() を再度呼べば Facet を再起動できます。別のクラスでも構いません。そのため abort() はコード更新に向いています。古いバージョンを動かしている Facet を abort し、新しいクラスを返すコールバックで get() を呼びます。
this.ctx.facets.delete(name string) void
Facet を abort し(稼働中なら)、その SQLite データベースを完全に削除します。あとから同じ名前で get() を呼ぶと、Facet は空のデータベースで開始します。
不要になった Facet のストレージを片付けるときは delete() を使います。
getStartupOptions コールバックが返すオブジェクトです。
Facet としてインスタンス化する Durable Object クラスです。Dynamic Worker スタブで worker.getDurableObjectClass("ClassName") を呼んで取得します。
Facet が自分の ctx.id として見る ID です。省略すると、Facet は親 Durable Object の ID を継承します。
スーパーバイザーと各 Facet は、別々の SQLite データベースを持ちます。動的コードは標準の Durable Object ストレージ API を使い、操作はすべて Facet 自身のデータベースに対して行われます。
この分離があるため、動的コードにスーパーバイザーのデータを任せる必要はありません。メタデータ、課金カウンター、アクセス制御の状態はスーパーバイザーのデータベースに置けます。Facet はそれらを読んだり変更したりできません。
本番では、動的コード自体をスーパーバイザーのデータベースに保存し、#loadDynamicWorker() メソッドで読み込むのが一般的です。こうすると、コードとそのコードを管理する Durable Object インスタンスが対になります。