Cloudflare D1
Das D1-Backend liefert alle drei Persistence-Komponenten gegen eine Cloudflare-D1-Datenbank:
D1Journal— das Event-Journal fürPersistentActors.D1SnapshotStore— Snapshots, um die Recovery zu begrenzen.D1DurableStateStore— Key-Value-Durable-State fürDurableStateActors.
Zwei Dinge sind daran ungewöhnlich:
- Es braucht kein Paket. D1 hat kein Node-SDK — außerhalb eines Workers ist es eine REST-API — das Backend spricht diese API also mit dem HTTP-Client des Frameworks. Es ist das einzige Backend, das deinem Dependency-Baum nichts hinzufügt.
- Seine Verifikation endet bei einem Fake. D1 hat keinen Emulator, der sich in einen Container heben lässt; anders als bei jedem anderen Backend gibt es daher keine Live-Integrations-Suite. Siehe Was verifiziert ist und was nicht — das steht bewusst vorne, weil es beeinflussen sollte, wie du es einsetzt.
Das SQL ist mit dem SQLite- und dem libSQL-Backend geteilt, das Schema ist über alle drei identisch, und eine Datenbank kann ohne Migration zwischen ihnen wandern.
Installation
Abschnitt betitelt „Installation“Nichts zu installieren:
bun add actor-tsDu brauchst eine D1-Datenbank und ein API-Token mit der
D1:Edit-Berechtigung, dazu deine Account-ID und die UUID der Datenbank
(nicht ihren Namen) — alle drei findest du im Cloudflare-Dashboard oder über
wrangler d1 info <name>.
Einrichtung
Abschnitt betitelt „Einrichtung“import { ActorSystem, ActorSystemOptions, D1DurableStateStoreOptions, D1JournalOptions, D1SnapshotStoreOptions, PersistenceExtensionId, RegisterD1PluginsOptions, registerD1Plugins,} from 'actor-ts';
const systemOptions = ActorSystemOptions.create() // Die D1-Plugins als aktives Journal + Snapshot-Store auswählen. .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);Das API-Token geht als Bearer-Token raus — behandle es wie jedes andere Secret und lies es aus der Umgebung, statt es zu committen. Nur einen Teil der drei Credentials zu setzen wird beim Verdrahten abgelehnt, denn das ist fast immer eine vergessene Umgebungsvariable.
Wann du es einsetzt
Abschnitt betitelt „Wann du es einsetzt“- Deine Daten müssen in D1 liegen. Ein Workers-nahes Deployment, in dem D1 ohnehin das System of Record ist.
- Du willst null Dependencies. Kein Treiber, kein nativer Build, nichts zu auditieren.
Für alles andere ist libSQL / Turso die bessere Wahl: gleiches Schema, gleiche SQLite-Semantik, ein echter Client, interaktive Transaktionen und eine Live-Suite dahinter.
Konfiguration
Abschnitt betitelt „Konfiguration“type D1Connection = { accountId?: string; // Cloudflare-Account-ID databaseId?: string; // die UUID der Datenbank, nicht ihr Name apiToken?: string; // Token mit D1:Edit baseUrl?: string; // Override für einen Proxy; Default api.cloudflare.com timeoutMs?: number; // Timeout pro Request, Default 30_000 client?: D1ClientLike; // vorgebauter / geteilter 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; // neueste N pro pid; Default 3, <=0 behält alle autoCreateTables?: boolean;}interface D1DurableStateStoreOptions extends D1Connection { table?: string; // Default 'durable_state' autoCreateTables?: boolean;}Identisch mit dem des
SQLite-Backends — events,
events_tags, events_meta, snapshots und durable_state, beim ersten
Zugriff mit CREATE TABLE IF NOT EXISTS angelegt.
Concurrency-Modell
Abschnitt betitelt „Concurrency-Modell“- Die Optimistic Concurrency des Journals beruht auf dem Primary Key.
appendliest den aktuellen Head und fügt ein; ein konkurrierender Writer verletzt den(persistence_id, sequence_nr)-Primary-Key, und das resultierendeUNIQUE constraint failedwird in einenJournalConcurrencyErrorübersetzt. - Durable-State-CAS nutzt
INSERT … ON CONFLICT DO NOTHINGzum Anlegen undUPDATE … WHERE revision = expectedzum Aktualisieren, wobei D1smeta.changesals Anzahl betroffener Zeilen gelesen wird.
Was verifiziert ist und was nicht
Abschnitt betitelt „Was verifiziert ist und was nicht“Jedes andere Backend im Framework hat eine Live-Integrations-Suite, die den
vollen Persistence-Contract in CI gegen einen echten Server fährt. D1 hat das
nicht und kann es derzeit nicht: es gibt kein D1-Container-Image, und lokal
existiert es nur innerhalb von wrangler/Miniflare — einer Workers-Runtime, nicht
einer Datenbank, die man in Compose startet.
Also präzise:
| Schicht | Wie sie verifiziert ist |
|---|---|
| Das SQL | Mit SQLite und libSQL geteilt und von deren Suiten gegen echtes SQLite ausgeführt |
| Die drei Storage-Contracts | Die geteilte Contract-Suite, gegen einen In-Process-Fake |
| REST-Envelope und Fehlerbehandlung | Unit-Tests gegen ein gestubbtes fetch |
| End-to-End gegen echtes D1 | Nicht verifiziert |
Fallstricke
Abschnitt betitelt „Fallstricke“Wie geht’s weiter
Abschnitt betitelt „Wie geht’s weiter“- libSQL / Turso — dasselbe Schema mit echtem Client, Transaktionen und Live-Testabdeckung.
- SQLite-Journal — das lokale Geschwister.
- Persistence-Überblick — die Backend-Matrix.
