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 Exec statement and flushes the batch as one BEGIN/COMMIT request on Commit. Rollback discards the buffer, since no server-side state exists to revert.
  • Queries inside a transaction error. A QueryContext issued 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 nil bound value returns errNullParamUnsupported. 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 SQL NULL through a generated query fails at runtime. Encode NULL in the statement text instead. The driver rejects the bind so it does not store empty text and corrupt IS NULL and COALESCE semantics.

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:

Companion engine:

  • SQLite engine, the dialect parser and migration dialect that pair with this driver.

Framework docs:

External: