Dynamic worker capabilities (v1)
Your deployed worker runs inside an isolated facet of the per-space
UnfolderSpace Durable Object. the platform injects a small, capability-scoped set
of loopbacks — each one is locked to your space and can't reach another:
env.MEDIA— a capability for your own space's media, two methods:sign(path, { expiresIn })→ a signed, time-limited space-host URL the supervisor verifies and serves. Gate private (_*) media per-identity: decide in your worker, thenResponse.redirectto the signed URL — you never proxy the bytes. (Public media serves by default; you don't needenv.MEDIA.)put(path, bytes, { contentType })→ store an inbound upload (avatar, PDF, export) and get back{ path, url, private }. Visibility follows the path — a normal path is public, a_-prefixed one private — so your user-facing logic picks where each user file lands. Overwrites by key; you authenticate + size-check the uploader. See media.
env.<YOUR_VARS>/env.<YOUR_SECRETS>— every Variable and Secret you declared is injected intoenvat deploy time (a secret's value is set by the owner in-browser and decrypted intoenvfor your code to read; see secrets).- Outbound
fetch()is routed through a per-space egress gateway: your own site/preview hosts are always allowed; every other host is default-deny until the owner approves it. Secrets and egress are independent — a secret is justenv, and your code builds any auth header it needs. See secrets.
Isolated storage state is the facet's own Durable Object storage, below which provide sqlite and kv dialects.
Free per-facet storage: SQLite + KV
Every facet has its own Durable Object storage — both a SQL interface
(ctx.storage.sql) and a simple KV interface (ctx.storage.kv) backed
by the same SQLite engine.
import { DurableObject } from "cloudflare:workers";
export class App extends DurableObject {
fetch(request) {
// Simple KV (great for counters, flags, small JSON):
let counter = this.ctx.storage.kv.get("counter") || 0;
++counter;
this.ctx.storage.kv.put("counter", counter);
// Or use SQL directly via this.ctx.storage.sql.exec(...) — see
// https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/
return new Response("You have made " + counter + " requests.\n");
}
}
SQL with bound parameters
ctx.storage.sql.exec(query, ...params) binds positional ? placeholders
to the trailing arguments. Always pass user-supplied values this way;
do not string-interpolate them into the query (SQL injection in your own
app).
import { DurableObject } from "cloudflare:workers";
export class App extends DurableObject {
constructor(ctx, env) {
super(ctx, env);
// CREATE TABLE IF NOT EXISTS is idempotent — safe to run on every
// cold start.
ctx.storage.sql.exec(`CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
author TEXT NOT NULL,
body TEXT NOT NULL,
created_at INTEGER NOT NULL
)`);
}
async fetch(request) {
const url = new URL(request.url);
if (request.method === "POST") {
const form = await request.formData();
// Parameterized INSERT — values bound to `?` placeholders.
this.ctx.storage.sql.exec(
"INSERT INTO notes (author, body, created_at) VALUES (?, ?, ?)",
String(form.get("author") ?? "anon"),
String(form.get("body") ?? ""),
Date.now(),
);
return new Response(null, { status: 303, headers: { Location: "/" } });
}
// Parameterized SELECT — cursor → array.
const rows = [...this.ctx.storage.sql
.exec("SELECT id, author, body FROM notes ORDER BY id DESC LIMIT ?", 50)
.toArray()];
return Response.json(rows);
}
}
Storage lifecycle:
- Production facet: SQLite preserved across redeploys. Use it for
user data, durable counters, anything that must survive a
deploy. - Preview facet: SQLite reset per redeploy (the facet is
delete'd thenget'd). Treat it as ephemeral.
This SQLite lives in the deployed worker, not the run_code source Workspace.
To read it from run_code, use introspect.query({ sql, params?, facet? }) —
a single read-only SELECT/PRAGMA/EXPLAIN against the live facet (defaults to
production). To write/seed it, expose an endpoint on your worker and call it
over HTTP. introspect works as long as your App keeps the scaffold's
__query export (see admin-prep).
To gate content by identity (member-only pages, an owner-only admin), run your
own auth in this storage — start the unfolder.auth prompt. Every facet boots
with nodejs_compat, so libraries like better-auth work in-facet.
Naming convention
The bundler facet host resolves your DO class via
getDurableObjectClass("App") — so it must be a named export called
App. export default class App will fail at facet startup.
Cross-space calls
If your app needs to call other unfolder.space spaces, just fetch them
over HTTP — that's the cross-space integration pattern (see
multi-space). To call a sibling space you own
with a shared secret, see the securing section in
multi-space.
Outbound network, variables & secrets
Outbound fetch() is default-deny (your own platform URLs are always
allowed), and a space configures variables, secrets, and per-host outbound rules
through config.* in run_code. That's its own surface — see
secrets for the full model (setVariable,
declareSecret, allowEgress, list, remove) and when to reach for each.