How does @opencode-ai/sdk/workerd use SQLite storage?
how does the @opencode-ai/sdk/workerd work? it uses sqlite storage, but how?
@opencode-ai/sdk/workerd does not ship or open a SQLite file. It adapts OpenCode’s database layer to the SQLite database built into each Cloudflare Durable Object.
###Data flow
OpenCode/Drizzle query
→ Effect SqlClient
→ sqlite.workerd adapter
→ state.storage.sql.exec(sql, ...bindings)
→ Durable Object’s persistent SQLite database
When you create the SDK:
OpenCodeWorkerd.create({
storage: state.storage,
})
the following happens:
-
OpenCodeWorkerd.create()builds the Workerd profile
(packages/sdk/src/workerd.ts,packages/sdk/src/internal/workerd.ts). -
The profile replaces the normal file-backed database service with:
Database.configuredClient( sqliteLayer({ storage: options.storage }), )See
packages/server/src/workerd.ts. -
sqliteLayer()implements Effect’sSqlClientusing:storage.sql.exec(query, ...params)See
packages/core/src/database/sqlite.workerd.ts. -
The regular OpenCode Drizzle database and migrations are then built on top of that client. On the first boot, OpenCode creates its schema and a
migrationtable; subsequent boots apply only missing migrations. Seepackages/core/src/database/database.tsandmigration.ts.
So this is Durable Object SQLite, not D1, KV, or a database stored in /tmp.
##One database per Durable Object ID
Each Durable Object instance has its own isolated SQLite database. For example:
const id = env.OPENCODE.idFromName("team-123")
const stub = env.OPENCODE.get(id)
All calls routed to "team-123" use that object’s database. A different name or ID gets a different database.
When Cloudflare evicts the object:
- the JavaScript isolate and in-memory SDK host disappear;
- the Durable Object SQLite data remains;
- constructing the object again reconnects to the same
state.storage; - OpenCode runs its migration/bootstrap checks and resumes from persisted state.
##Transactions
Cloudflare’s SQLite API rejects transaction SQL such as BEGIN, COMMIT, and SAVEPOINT. The adapter therefore does not emit those statements.
Instead, it maps OpenCode transactions onto:
state.storage.transaction(async (txn) => {
// SQL operations
// txn.rollback() if the Effect fails
})
A semaphore serializes use of the single connection. Nested transactions are explicitly rejected because Durable Object SQLite does not provide the savepoint behavior expected by the normal adapter.
The implementation is in packages/core/src/database/sqlite.workerd.ts, particularly makeWithTransaction().
##What gets stored
It is the normal OpenCode relational schema, including things such as:
- sessions (
session_v2); - messages and session inbox entries;
- durable events (
event,event_sequence); - projects and workspaces;
- permissions and credentials;
- migration history.
For Workerd, event persistence is forcibly enabled:
events: { persist: true }
This matters because Cloudflare can evict the object in the middle of a model turn. On the next wake, OpenCode can inspect durable event/session history and resume orphaned work.
##Required Wrangler configuration
The Durable Object class must be declared as a SQLite-backed class:
{
"compatibility_flags": ["nodejs_compat"],
"durable_objects": {
"bindings": [
{
"name": "OPENCODE",
"class_name": "OpenCodeDO"
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["OpenCodeDO"]
}
]
}
The repository’s packaged-SDK verification uses this configuration in packages/sdk/script/verify-package.ts.
The host should be initialized once per object lifetime:
export class OpenCodeDO {
private readonly opencode: Promise<OpenCodeWorkerd.Interface>
constructor(state: DurableObjectState) {
this.opencode = state.blockConcurrencyWhile(() =>
OpenCodeWorkerd.create({
storage: state.storage,
}),
)
}
async fetch() {
const opencode = await this.opencode
return Response.json(await opencode.health.get())
}
}
blockConcurrencyWhile() prevents requests from reaching a partially initialized database/application graph.
##Important limitation
SQLite stores OpenCode’s application/session state, but it does not turn the Durable Object into a local coding sandbox. In the Workerd profile:
- local filesystem access is unavailable;
- shell/process spawning is unavailable;
- PTYs are unavailable;
- filesystem search is unavailable;
- snapshots are no-ops;
- plugins must already be bundled into the Worker.
Those services would need a remote workspace/sandbox provider. The SQLite database is the durable control/session plane, not repository file storage.
ahh, so it doesn't have a file system. ok interesting
Exactly. The Workerd profile is currently a durable session/control plane, not a complete local coding environment.
SQLite persists sessions, messages, events, permissions, and recovery state, but Workerd has no usable project filesystem or process execution. Consequently:
FileSystem.read/list/findfail as unavailable- shell commands cannot run
- PTYs cannot be created
- local filesystem search is unavailable
- snapshots and VCS mostly return no-op/empty results
To use it as a functional coding agent, you’d provide a remote workspace/sandbox provider that supplies filesystem and command execution. The Durable Object would then coordinate the agent and retain its state while the external sandbox holds the repository and runs commands.
END OF POST