跳转到内容
简体中文

Cloudflare D1

此内容尚不支持你的语言。

The D1 backend provides all three persistence components against a Cloudflare D1 database:

  • D1Journal — the event journal for PersistentActors.
  • D1SnapshotStore — snapshots to bound recovery.
  • D1DurableStateStore — key-value durable state for DurableStateActors.

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.

Nothing to install:

Terminal window
bun add actor-ts

You 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.

  • 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.

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;
}

Identical to the SQLite backend’sevents, events_tags, events_meta, snapshots and durable_state, created with CREATE TABLE IF NOT EXISTS on first use.

  • The journal’s optimistic concurrency rests on the primary key. append reads the current head and inserts; a racing writer trips the (persistence_id, sequence_nr) primary key, and the resulting UNIQUE constraint failed is translated into JournalConcurrencyError.
  • Durable-state CAS uses INSERT … ON CONFLICT DO NOTHING to create and UPDATE … WHERE revision = expected to update, reading D1’s meta.changes as the affected-row count.

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:

LayerHow it is verified
The SQLShared with SQLite and libSQL, and exercised against a real SQLite by their suites
The three storage contractsThe shared contract suite, against an in-process fake
The REST envelope and error handlingUnit tests against a stubbed fetch
End-to-end against real D1Not verified