Zum Inhalt springen
Deutsch

Cloudflare D1

Das D1-Backend liefert alle drei Persistence-Komponenten gegen eine Cloudflare-D1-Datenbank:

  • D1Journal — das Event-Journal für PersistentActors.
  • D1SnapshotStore — Snapshots, um die Recovery zu begrenzen.
  • D1DurableStateStore — Key-Value-Durable-State für DurableStateActors.

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.

Nichts zu installieren:

Terminal-Fenster
bun add actor-ts

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

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.

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

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-Backendsevents, events_tags, events_meta, snapshots und durable_state, beim ersten Zugriff mit CREATE TABLE IF NOT EXISTS angelegt.

  • Die Optimistic Concurrency des Journals beruht auf dem Primary Key. append liest den aktuellen Head und fügt ein; ein konkurrierender Writer verletzt den (persistence_id, sequence_nr)-Primary-Key, und das resultierende UNIQUE constraint failed wird in einen JournalConcurrencyError übersetzt.
  • Durable-State-CAS nutzt INSERT … ON CONFLICT DO NOTHING zum Anlegen und UPDATE … WHERE revision = expected zum Aktualisieren, wobei D1s meta.changes als Anzahl betroffener Zeilen gelesen wird.

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:

SchichtWie sie verifiziert ist
Das SQLMit SQLite und libSQL geteilt und von deren Suiten gegen echtes SQLite ausgeführt
Die drei Storage-ContractsDie geteilte Contract-Suite, gegen einen In-Process-Fake
REST-Envelope und FehlerbehandlungUnit-Tests gegen ein gestubbtes fetch
End-to-End gegen echtes D1Nicht verifiziert