Skip to content
Storage / D1

Storage

D1

Use D1 databases with OpenOrange, backed by a local Rivet Actor with SQLite.

Status: Supported. Runs on one Rivet Actor for each database.

Configuration

OpenOrange reads the standard Wrangler D1 binding. No extra setup is needed:

{
  "d1_databases": [
    { "binding": "DB", "database_name": "app", "database_id": "<DATABASE_ID>" }
  ]
}

Like wrangler dev, OpenOrange uses preview_database_id when it is set, then database_id, then the binding name. Bindings with the same ID share data.

How it works

Each database is one internal Durable Object, which runs as a Rivet Actor with SQLite storage. Data persists across orange dev restarts in the project’s .open-orange directory.

The client is a port of workerd’s D1 binding, and the backend follows the local D1 backend of wrangler dev:

  • prepare(), bind(), first(), all(), run(), and raw(), with the same result shapes and meta fields.
  • batch() runs all statements in one transaction. If one statement fails, none of them apply.
  • exec() runs each line as one statement.
  • withSession() returns a session whose bookmark advances after each query.
  • Errors use D1’s prefixes, for example D1_ERROR: no such table: users: SQLITE_ERROR.

Commands

These commands work on the same local database that orange dev uses:

orange d1 migrations list DB
orange d1 migrations apply DB
orange d1 execute DB --command "SELECT * FROM users"
orange d1 execute DB --file ./seed.sql --json

Like Wrangler, the commands find the database by binding or database_name and target the local database when --remote is not given. Migrations come from migrations_dir (default migrations), optionally filtered by migrations_pattern, and run in the order of their leading number. The migrations_table (default d1_migrations) records each applied migration. Each migration runs as one batch with its record, so a failed migration rolls back and the later ones do not run.

Each command briefly starts the OpenOrange runtime, which writes through the same internal Durable Object as orange dev.

With --remote, the commands target the database that orange serve uses: the Rivet Engine in RIVET_ENDPOINT. Run them from the project with the same Worker name and --env as the deployment:

RIVET_ENDPOINT=https://<namespace>:<token>@api.rivet.dev orange d1 migrations apply DB --remote

The Engine runs the database on the deployed orange serve, so the deployment must be up and its pool configured.

Conformance

An end-to-end test applies migrations with orange d1, including a failed migration that rolls back, and reads the result through orange dev. An end-to-end orange dev test covers result shapes, meta, BLOB and boolean bindings, batch rollback, error text, sessions, overlapping inserts, and persistence across restarts. The same Worker returns identical results under wrangler dev. The upstream workerd D1 tests are not part of the conformance corpus.

Limits and deviations

  • DELETE ... LIMIT and UPDATE ... LIMIT fail, with or without RETURNING and ORDER BY. Cloudflare D1 accepts them. Orange’s SQLite comes from RivetKit, which builds SQLite 3.46.0 from its amalgamation without SQLITE_ENABLE_UPDATE_DELETE_LIMIT, so the parser has no LIMIT for these statements. The query fails with:

    D1_ERROR: near "LIMIT": syntax error at offset <n>: SQLITE_ERROR

    chanfana before 3.1.0 deletes a row with DELETE ... RETURNING * LIMIT 1, so every D1DeleteEndpoint of such an app answers 500 on Orange. Upgrade chanfana to 3.1.0 or later, which deletes without LIMIT. orange doctor flags an older chanfana in a project with a D1 database. In your own SQL, select the rows in a subquery instead: DELETE FROM t WHERE rowid IN (SELECT rowid FROM t WHERE ... LIMIT 1).

  • --remote reads RIVET_ENDPOINT, not a Cloudflare account. --preview and --persist-to fail.

  • orange d1 migrations apply does not ask for confirmation and does not capture a backup.

  • meta.rows_read and meta.rows_written count returned rows and changed rows. They do not include index reads and writes.

  • sqlite_master also lists OpenOrange’s internal _rivet_ tables. User SQL cannot read or change them.

  • A prepared statement with several statements returns the last statement’s rows. wrangler dev rejects it.

  • dump() fails with D1_DUMP_ERROR, like wrangler dev.

  • query() requires the d1_binding_jsrpc compatibility flag, which OpenOrange does not apply, so it always fails.

  • Read replication is not simulated. Every query reaches the single local database.

Compatibility

From the latest conformance run. Scores on the compatibility page; every binding on the status page.

D1

Drop-in d1_databases

Runs on Internal Rivet Actor per database, on the Durable Object SQL bridge.

meta.rows_read counts returned rows, not scanned rows, and meta.rows_written misses index and trigger writes.

Scales by Database. One actor per database. A busy database is one actor, so split by tenant for more.

Recorded results

WorksD1Available.prepare, batch, and migrations work, on the SQLite build above. Everything in the SQLite section applies. 2/2 pass
Results
  • example: d1-migrations Pass
  • sqlite-surface: D1: batch() runs statements in order Pass workerd pass

In the feature matrix

Alternatived1_databases (D1)Binds a D1 database: managed SQLite with optional read replicas.Each database id is an Orange-internal SQLite Durable Object on Rivet Actors. Data persists under .open-orange/rivetkit across orange dev restarts. 7 files
Notes
Every query goes to one local primary, so there are no read replicas. The local id is preview_database_id, then database_id, then the binding name. The D1 client follows workerd's HTTP-based client, and the backend follows Miniflare's D1 backend. orange d1 migrations apply/list and orange d1 execute write the same local store that orange dev reads.
Source
src/config/load-wrangler.ts:803-808packages/runtime-node/src/runner.ts:584-591packages/runtime-node/src/bindings/internal-objects.ts:12-21packages/runtime-node/src/bindings/d1/database.ts:1-13packages/runtime-node/src/bindings/d1/object.ts:5-8src/runtime/bindings.test.ts:236-286src/commands/d1.test.ts:29-180
SupportedD1 prepare()Makes a prepared statement from SQL.Makes a statement that runs on the D1 Rivet Actor. 4 files
SupportedD1 bind()Binds numbers, strings, booleans, null, and byte arrays.Same conversions as workerd: booleans become 1 or 0, and ArrayBuffers become byte arrays. Other types fail with D1_TYPE_ERROR. 4 files
SupportedD1 first()Returns the first row, one column of it, or null.Same result. A missing column fails with D1_COLUMN_NOTFOUND: Column not found (<name>). 3 files
SupportedD1 all()Returns results and meta for a statement.Returns results and meta with changes, last_row_id, rows_read, rows_written, size_after, and duration. 3 files
SupportedD1 raw()Returns rows as arrays, with an optional first row of column names.Same result, including the columnNames option. 2 files
SupportedD1 run()Runs a statement and returns meta.Runs the statement through the /execute path and returns the backend result. 2 files
SupportedD1 batch()Runs statements in one transaction. A failure rolls back the whole batch.Runs the statements in one storage transaction. A failure rolls back the batch. 3 files
SupportedD1 exec()Runs raw SQL, one statement per line, and returns count and duration.Splits the input by line like workerd and reports the failing line in D1_EXEC_ERROR. 4 files
UnsupportedD1 dump()Deprecated. Returns the database file for databases from the D1 alpha.The backend has no dump route, so dump() always rejects with D1_DUMP_ERROR: Status + 404. 2 files
Notes
The error does not say that Orange does not support dump().
Not verified by a test
Not run. Follows from the backend routing only /query and /execute.
Source
packages/runtime-node/src/bindings/d1/database.ts:259-275packages/runtime-node/src/bindings/d1/object.ts:75-82
SupportedD1 withSession()Sessions give sequential consistency across read replicas.Sessions work. Every query reaches the one primary, so reads are always current. 4 files
SupportedD1 session getBookmark()Returns the latest bookmark of the session.Returns a local bookmark made from a timestamp and a counter. 4 files