ADR-004: Migrate Database from Turso to Cloudflare D1
Status: Accepted Date: 2026-04-02 Deciders: CTO
Context
CascadeGuard API runs as a Cloudflare Worker (Python/Pyodide). The initial implementation used Turso (libSQL hosted service) as the primary database. Every SQL query required an outbound HTTPS fetch from the Worker to Turso’s edge, adding latency and requiring an auth token to be managed as a secret.
With the introduction of the public image catalog endpoints (CAS-30) the query volume and latency requirements changed:
- Dashboard queries are latency-sensitive. Each page load may issue multiple aggregate queries (image list, vuln counts, SLA status).
- Secret management overhead. The Turso auth token had to be stored in Workers secrets and rotated periodically.
- Cold-start penalty. Turso’s HTTP client added ~50-100 ms per request on cold Workers, on top of the SQL round-trip.
Decision
Replace Turso with Cloudflare D1 — Cloudflare’s native SQLite database
that is bound directly to the Worker via wrangler.toml.
Consequences
Benefits
- Zero-latency binding. D1 is co-located with the Worker; SQL calls are local function invocations, not network requests. Dashboard queries see single-digit-ms response times.
- No secrets required. The
DBbinding is declared inwrangler.tomland wired automatically — no auth tokens to manage or rotate. - Simpler dependency graph. Removes the Turso HTTP client and its
configuration (URL, token). The
D1Clientwrapper indb/d1.pyis ~30 lines vs. ~80 for the oldTursoClient. - Native Cloudflare ecosystem. D1 integrates with Workers analytics, logging, and the dashboard — no third-party observability needed.
- SQLite compatibility. The existing schema (migrations/) and all SQL queries are standard SQLite and required no changes.
Trade-offs
- D1 is in open beta. Cloudflare may change pricing, limits, or behaviour. Acceptable for an MVP; revisit before GA pricing takes effect.
- Single-region write leader. D1 has a single write location with read replicas. Write throughput is lower than Turso’s multi-region replication. Acceptable because CascadeGuard’s write path (scan ingestion) is low-volume compared to reads (public dashboard).
- No libSQL extensions. Turso supports libSQL vector search and other extensions. We do not use any of these today, but if needed in the future a migration back (or to a different store) would be required.
- Database size limit. D1 free tier allows 500 MB; paid plan allows up to 10 GB per database. Sufficient for image/scan/vulnerability metadata. If SBOM document storage were moved into D1 (currently in R2) this limit would need monitoring.
Migration Notes
- The
D1Clientindb/d1.pyexposes the sameexecute()andbatch()interface as the oldTursoClient, so route handlers required no changes beyond swapping the import. - The
wrangler.tomlbinding name isDB(matching the old env var convention) soenv.DBworks identically. - Existing migrations in
migrations/are SQLite-compatible and apply to D1 without modification viawrangler d1 migrations apply.