Cloudflare D1 driver
database/sql driver for Cloudflare D1, Cloudflare's edge SQLite database accessed over their HTTP API.
Overview
D1 is SQLite hosted on Cloudflare's network. It speaks SQLite SQL but lives behind an HTTPS API instead of a local file or socket. This driver registers a database/sql driver named d1. Its connection backend translates Query and Exec calls into Cloudflare API requests. From your application's perspective it looks like a standard *sql.DB. It pairs with piko.WithDatabase through the same DatabaseRegistration shape as any other SQLite-compatible engine. It reuses the SQLite engine, the SQLite codegen, and the SQLite migration dialect with no D1-specific glue. A project can move from a local SQLite file to D1 by swapping only the connection.
D1's character is SQLite at the edge. Reach for it when you are already on the Cloudflare stack (Workers, Pages, R2, KV) and want the same locality story for relational data. It also fits when low storage and request costs at small scale matter, or when you want a managed SQLite without operating the disk yourself. Reach for SQLite (CGO) or SQLite (No CGO) when you control the host and a local file is fine. Reach for Postgres for richer SQL or higher concurrency. Reach for CockroachDB for distributed strong consistency on the Postgres dialect.
The HTTP transport sets the latency story. Every query is a network round-trip to a Cloudflare API endpoint. Round-trip time to the nearest D1-eligible Cloudflare node and the API rate limits dominate latency, not SQL execution time. Plan for batched queries and avoid chatty per-row patterns.
Requirements
- A Cloudflare account with D1 enabled.
- A D1 database created (via the dashboard or
wrangler d1 create). - An API token with D1 edit permission scoped to the target account/database.
- Network egress to
api.cloudflare.com.
Configuration
import (
"os"
"piko.sh/piko/wdk/db/db_driver_d1"
)
connection, err := db_driver_d1.Open(db_driver_d1.Config{
APIToken: os.Getenv("CF_API_TOKEN"), // required; D1:Edit scope
AccountID: os.Getenv("CF_ACCOUNT_ID"), // required; the Cloudflare account ID
DatabaseID: os.Getenv("CF_D1_DATABASE_ID"), // required; the D1 database UUID
})
if err != nil {
return err
}
Open validates that all three fields are non-empty and returns a configured *sql.DB ready to pair with the SQLite engine. The driver registers itself as d1 with database/sql at package init.
Bootstrap
import (
"piko.sh/piko"
"piko.sh/piko/wdk/db"
"piko.sh/piko/wdk/db/db_driver_d1"
"piko.sh/piko/wdk/db/db_engine_sqlite"
)
ssr := piko.New(
piko.WithDatabase("primary", &db.DatabaseRegistration{
DB: connection,
EngineConfig: db_engine_sqlite.SQLite(),
MigrationFS: migrationsFS,
}),
)
Piko notes
D1 reuses piko's SQLite machinery whole, but the HTTP transport changes three runtime behaviours the reader must design around.
- Transactions run client-side. D1 has no interactive transaction. The driver buffers each
Execstatement and flushes the batch as oneBEGIN/COMMITrequest onCommit.Rollbackdiscards the buffer, since no server-side state exists to revert. - Queries inside a transaction error. A
QueryContextissued while a transaction is open returns an error. A batch cannot return intermediate rows. Any action that opens a transaction and reads within it fails. Keep reads outside the transaction. - NULL bound parameters error. A
nilbound value returnserrNullParamUnsupported. The D1 wire format binds parameters as a JSON array of strings, which cannot carry a JSON null. The generated querier binds nullable columns as parameters, so writing a true SQLNULLthrough a generated query fails at runtime. EncodeNULLin the statement text instead. The driver rejects the bind so it does not store empty text and corruptIS NULLandCOALESCEsemantics.
Close is a no-op and each statement is an independent HTTPS call. There is no connection pool, ping, or replica to manage. Because you pass a pre-opened DB, the registration's pool settings (MaxOpenConns, MaxIdleConns, ConnMaxLifetime) are not applied, and they carry no D1 semantics in any case. Latency is per-call round-trip time.
Tradeoffs
D1 is SQLite server-side, with generated columns, FTS5, and JSON1 intact. The driver forwards your SQL unchanged and only stringifies parameters. The HTTP transport is the dominant cost on every query. Code that does N+1 lookups pays for each one. Batch your reads, prefer joins over per-row fetches, and treat the connection like a remote service instead of a local file. Co-locate the caller. A Worker calling D1 from the same colocation is fast. A long-running Go service in another cloud calling D1 is slow.
See also
Sibling drivers:
- SQLite Driver (CGO), local SQLite, full feature set, CGO required.
- SQLite Driver (No CGO), local SQLite, pure Go, cross-compiles cleanly.
Companion engine:
- SQLite engine, the dialect parser and migration dialect that pair with this driver.
Framework docs:
- How to use databases and queries, registering connections and running migrations.
- Database API reference, every type and function on the database service.
- About the database service, design rationale and the build-time vs runtime split.
External:
- Cloudflare D1 documentation, authoritative reference and limits.
- D1 REST API, the transport this driver uses.