Skip to main content

Data stores

The Worker stores two things: claims (one JSON document each) and feedback votes (one row per rated answer). Each sits behind a port with a D1 adapter and a Neon adapter. One setting, DATABASE_PROVIDER, picks both.

The adapter is chosen once per request. Nothing outside these folders knows which store is underneath.

The claims port​

api/src/claims/repository.ts (abridged)
export interface ClaimRepository {
readonly provider: 'd1' | 'neon' | 'none';
list(filter: ClaimFilter): Promise<{ items: ClaimSummary[]; total: number }>;
get(id: string): Promise<Claim | null>;
upsert(claims: Claim[]): Promise<number>;
scenarios(): Promise<ScenarioCount[]>;
randomId(filter: RandomFilter): Promise<string | null>;
}

list filters by line of business, currency, scenario and status, searches id, title and claimant name case-insensitively, and pages with limit (1 to 500, default 50) and offset. randomId can pick within a line or currency, or a claim whose demo notes showcase a given capability.

The table​

One table, claims: the id, four denormalised columns for listing (line_of_business, scenario, status, title), and the whole document in data. The document is the source of truth; the columns exist only for listing and filtering. Every read parses the document with Claim.parse, so defaults are applied on the way out.

Cloudflare D1Neon Postgres
Selected byDATABASE_PROVIDER=d1 (default) + the DB bindingDATABASE_PROVIDER=neon + DATABASE_URL
data columntext, read with json_extractjsonb, read with -> / ->>
Parameterspositional ?numbered $1, $2, …
Showcase filterjson_each over metadata.demo.showcasesJSONB containment @>
Upsertsbatches of 25 in one db.batch (one transaction)one statement per claim
Locallya SQLite file under api/.wrangler/statethe shared database
From the CLIwrangler d1 executethe same adapter as the Worker

No store at all​

DATABASE_PROVIDER=none gives an empty repository: list returns nothing, get returns null, upsert throws. That is the configuration for a deployment that only serves embedded hosts, where the caller always sends the claim. A claimId on such a deployment gets "Claim … not found", which is the truth.

The feedback port​

api/src/feedback/repository.ts (abridged)
export interface FeedbackRepository {
readonly provider: 'd1' | 'neon' | 'none';
record(entry: FeedbackEntry): Promise<void>; // insert, or replace by turn id
remove(turnId: string): Promise<void>; // a withdrawn vote
report(filter: FeedbackFilter): Promise<FeedbackReport>;
}

The feedback table is keyed by turn_id, so a changed vote replaces the old one. Alongside the vote's JSON in data sit columns for filtering and the report: rating, capability, claim_id, prompt_fingerprint, channel, surface, and timestamps. The report returns totals, a tally by capability, a tally by capability and prompt set, and the most recent votes (1 to 200, default 50).

With DATABASE_PROVIDER=none, each vote is written to the Worker log as one JSON line, {"feedback": {…}}, which Workers Logs or a Logpush job can collect. The report is empty.

SQLite returns timestamps as YYYY-MM-DD HH:MM:SS with no zone, and Postgres returns a Date. toIso() turns both into ISO 8601 UTC, so the wire format is the same.

Migrations​

StoreFolderApplied byCommand
D1db/migrations/d1/Wrangler's own migration trackingnpm run db:migrate (local), npm run db:migrate:remote
Neondb/migrations/neon/db/migrate.mjs, recording each file in schema_migrationsnpm run db:migrate:neon

Both folders carry the same numbered migrations: 0001_init.sql (claims) and 0002_feedback.sql. A new migration goes in both folders, written for each dialect. prompt_overrides in the Neon 0001 migration is a leftover from before prompt edits moved to the browser, and is unused.

Shared databases

The remote D1 database and Neon are shared. Never run destructive SQL against them as a matter of course, and never from a test.

Tests​

The D1 adapters are tested against real SQLite: api/test/d1.ts is a D1 binding over Node's in-memory node:sqlite with every D1 migration applied, so json_extract, json_each and on conflict … do update run as written. Like D1, binding undefined throws, so a missing ?? null fails a test. The Neon adapters are tested against a mocked driver for their SQL and placeholder numbering. See Unit and integration tests.

Adding a store​

See Add a data store.