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 DB binding is declared in wrangler.toml and wired automatically — no auth tokens to manage or rotate.
  • Simpler dependency graph. Removes the Turso HTTP client and its configuration (URL, token). The D1Client wrapper in db/d1.py is ~30 lines vs. ~80 for the old TursoClient.
  • 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 D1Client in db/d1.py exposes the same execute() and batch() interface as the old TursoClient, so route handlers required no changes beyond swapping the import.
  • The wrangler.toml binding name is DB (matching the old env var convention) so env.DB works identically.
  • Existing migrations in migrations/ are SQLite-compatible and apply to D1 without modification via wrangler d1 migrations apply.