Cloudflare D1
このコンテンツはまだ日本語訳がありません。
The D1 backend provides all three persistence components against a Cloudflare D1 database:
D1Journal— the event journal forPersistentActors.D1SnapshotStore— snapshots to bound recovery.D1DurableStateStore— key-value durable state forDurableStateActors.
Two things are unusual about it:
- It needs no package. D1 has no Node SDK — outside a Worker it is a REST API — so the backend speaks that API with the framework’s own HTTP client. It is the only backend that adds nothing to your dependency tree.
- Its verification stops at a fake. D1 has no emulator that can be brought up in a container, so unlike every other backend there is no live integration suite. See What is and is not verified — this is stated up front because it should affect how you adopt it.
The SQL is shared with the SQLite and libSQL backends, so the schema is identical across all three and a database can move between them without a migration.
Install
Section titled “Install”Nothing to install:
bun add actor-tsYou need a D1 database and an API token with the D1:Edit permission, plus
your account id and the database’s UUID (not its name) — all three are in the
Cloudflare dashboard or wrangler d1 info <name>.
import { ActorSystem, ActorSystemOptions, D1DurableStateStoreOptions, D1JournalOptions, D1SnapshotStoreOptions, PersistenceExtensionId, RegisterD1PluginsOptions, registerD1Plugins,} from 'actor-ts';
const systemOptions = ActorSystemOptions.create() // Select the D1 plugins as the active journal + snapshot store. .withConfig({ 'actor-ts': { persistence: { journal: { plugin: 'actor-ts.persistence.journal.cloudflare-d1' }, 'snapshot-store': { plugin: 'actor-ts.persistence.snapshot-store.cloudflare-d1' }, }, }, });const system = ActorSystem.create('my-app', systemOptions);
const ext = system.extension(PersistenceExtensionId);const d1SnapshotStoreOptions = D1SnapshotStoreOptions.create() .withKeepN(3);const registerOptions = RegisterD1PluginsOptions.create() .withAccountId(process.env.CLOUDFLARE_ACCOUNT_ID!) .withDatabaseId(process.env.D1_DATABASE_ID!) .withApiToken(process.env.CLOUDFLARE_API_TOKEN!) .withJournal(D1JournalOptions.create() /* .withEventsTable(...) */) .withSnapshotStore(d1SnapshotStoreOptions) .withDurableStateStore(D1DurableStateStoreOptions.create() /* .withTable(...) */);const { durableStateStore } = registerD1Plugins(ext, registerOptions);The API token is sent as a bearer token, so treat it like any other secret — read it from the environment rather than committing it. Setting only some of the three credentials is rejected at wiring time, since that is almost always a forgotten environment variable.
When to use it
Section titled “When to use it”- Your data has to live in D1. A Workers-adjacent deployment where D1 is already the system of record.
- You want zero dependencies. No driver, no native build, nothing to audit.
For anything else, prefer libSQL / Turso: same schema, same SQLite semantics, a real client, interactive transactions, and a live integration suite behind it.
Configuration
Section titled “Configuration”type D1Connection = { accountId?: string; // Cloudflare account id databaseId?: string; // the database UUID, not its name apiToken?: string; // token with D1:Edit baseUrl?: string; // override for a proxy; default api.cloudflare.com timeoutMs?: number; // per-request timeout, default 30_000 client?: D1ClientLike; // pre-built / shared transport};
interface D1JournalOptions extends D1Connection { eventsTable?: string; // default 'events' tagsTable?: string; // default '<eventsTable>_tags' autoCreateTables?: boolean; // default true}interface D1SnapshotStoreOptions extends D1Connection { snapshotsTable?: string; // default 'snapshots' keepN?: number; // keep newest N per pid; default 3, <=0 keeps all autoCreateTables?: boolean;}interface D1DurableStateStoreOptions extends D1Connection { table?: string; // default 'durable_state' autoCreateTables?: boolean;}Schema
Section titled “Schema”Identical to the SQLite backend’s —
events, events_tags, events_meta, snapshots and durable_state, created
with CREATE TABLE IF NOT EXISTS on first use.
Concurrency model
Section titled “Concurrency model”- The journal’s optimistic concurrency rests on the primary key.
appendreads the current head and inserts; a racing writer trips the(persistence_id, sequence_nr)primary key, and the resultingUNIQUE constraint failedis translated intoJournalConcurrencyError. - Durable-state CAS uses
INSERT … ON CONFLICT DO NOTHINGto create andUPDATE … WHERE revision = expectedto update, reading D1’smeta.changesas the affected-row count.
What is and is not verified
Section titled “What is and is not verified”Every other backend in the framework has a live integration suite running the
full persistence contract against a real server in CI. D1 does not, and cannot
as things stand: there is no D1 container image, and locally it exists only
inside wrangler/Miniflare — a Workers runtime rather than a database you can
start in compose.
So, precisely:
| Layer | How it is verified |
|---|---|
| The SQL | Shared with SQLite and libSQL, and exercised against a real SQLite by their suites |
| The three storage contracts | The shared contract suite, against an in-process fake |
| The REST envelope and error handling | Unit tests against a stubbed fetch |
| End-to-end against real D1 | Not verified |
Pitfalls
Section titled “Pitfalls”Where to next
Section titled “Where to next”- libSQL / Turso — the same schema with a real client, transactions and live test coverage.
- SQLite journal — the local sibling.
- Persistence overview — the backend matrix.
