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
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 D1 | Neon Postgres | |
|---|---|---|
| Selected by | DATABASE_PROVIDER=d1 (default) + the DB binding | DATABASE_PROVIDER=neon + DATABASE_URL |
data column | text, read with json_extract | jsonb, read with -> / ->> |
| Parameters | positional ? | numbered $1, $2, … |
| Showcase filter | json_each over metadata.demo.showcases | JSONB containment @> |
| Upserts | batches of 25 in one db.batch (one transaction) | one statement per claim |
| Locally | a SQLite file under api/.wrangler/state | the shared database |
| From the CLI | wrangler d1 execute | the 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
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
| Store | Folder | Applied by | Command |
|---|---|---|---|
| D1 | db/migrations/d1/ | Wrangler's own migration tracking | npm run db:migrate (local), npm run db:migrate:remote |
| Neon | db/migrations/neon/ | db/migrate.mjs, recording each file in schema_migrations | npm 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.
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.