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(), andraw(), with the same result shapes andmetafields.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 ... LIMITandUPDATE ... LIMITfail, with or withoutRETURNINGandORDER BY. Cloudflare D1 accepts them. Orange’s SQLite comes from RivetKit, which builds SQLite 3.46.0 from its amalgamation withoutSQLITE_ENABLE_UPDATE_DELETE_LIMIT, so the parser has noLIMITfor these statements. The query fails with:D1_ERROR: near "LIMIT": syntax error at offset <n>: SQLITE_ERRORchanfana before 3.1.0 deletes a row with
DELETE ... RETURNING * LIMIT 1, so everyD1DeleteEndpointof such an app answers 500 on Orange. Upgrade chanfana to 3.1.0 or later, which deletes withoutLIMIT.orange doctorflags 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). -
--remotereadsRIVET_ENDPOINT, not a Cloudflare account.--previewand--persist-tofail. -
orange d1 migrations applydoes not ask for confirmation and does not capture a backup. -
meta.rows_readandmeta.rows_writtencount returned rows and changed rows. They do not include index reads and writes. -
sqlite_masteralso 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 devrejects it. -
dump()fails withD1_DUMP_ERROR, likewrangler dev. -
query()requires thed1_binding_jsrpccompatibility 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
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
- Notes
- Byte arrays are stored as BLOBs and read back as arrays of numbers.
- Source
- packages/runtime-node/src/bindings/d1/database.ts:75-77packages/runtime-node/src/bindings/d1/database.ts:289-481packages/runtime-node/src/bindings/d1/object.ts:50-61src/runtime/bindings.test.ts:236-256
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
- Notes
- Byte arrays are stored as BLOBs and read back as arrays of numbers.
- Source
- packages/runtime-node/src/bindings/d1/database.ts:75-77packages/runtime-node/src/bindings/d1/database.ts:289-481packages/runtime-node/src/bindings/d1/object.ts:50-61src/runtime/bindings.test.ts:236-256
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
- Notes
- Matches workerd's client code.
- Source
- packages/runtime-node/src/bindings/d1/database.ts:316-332src/runtime/fixtures/bindings/worker.js:135-139src/runtime/fixtures/bindings/worker.js:173
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
- Notes
- meta.served_by is "open-orange.db", and served_by_primary is always true.
- Source
- packages/runtime-node/src/bindings/d1/database.ts:346-354packages/runtime-node/src/bindings/d1/object.ts:121-158src/runtime/bindings.test.ts:236-256
SupportedD1 raw()Returns rows as arrays, with an optional first row of column names.Same result, including the columnNames option. 2 files
- Notes
- Matches workerd's client code.
- Source
- packages/runtime-node/src/bindings/d1/database.ts:356-380src/runtime/fixtures/bindings/worker.js:146
SupportedD1 run()Runs a statement and returns meta.Runs the statement through the /execute path and returns the backend result. 2 files
- Notes
- Like Miniflare, the result also has results.
- Source
- packages/runtime-node/src/bindings/d1/database.ts:334-344src/runtime/fixtures/bindings/worker.js:126
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
- Notes
- Overlapping requests keep every insert.
- Source
- packages/runtime-node/src/bindings/d1/database.ts:141-153packages/runtime-node/src/bindings/d1/object.ts:101-110src/runtime/bindings.test.ts:258-277
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
- Notes
- Empty input fails with No SQL statements detected.
- Source
- packages/runtime-node/src/bindings/d1/database.ts:239-257packages/runtime-node/src/bindings/d1/object.ts:101-110src/runtime/fixtures/bindings/worker.js:115src/runtime/fixtures/bindings/worker.js:174
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
- Notes
- Bookmarks are local strings made from a timestamp and a counter. A bookmark from Cloudflare has no meaning in Orange.
- Source
- packages/runtime-node/src/bindings/d1/database.ts:87-93packages/runtime-node/src/bindings/d1/database.ts:110-200packages/runtime-node/src/bindings/d1/object.ts:160-167src/runtime/fixtures/bindings/worker.js:150-153
SupportedD1 session getBookmark()Returns the latest bookmark of the session.Returns a local bookmark made from a timestamp and a counter. 4 files
- Notes
- Bookmarks are local strings made from a timestamp and a counter. A bookmark from Cloudflare has no meaning in Orange.
- Source
- packages/runtime-node/src/bindings/d1/database.ts:87-93packages/runtime-node/src/bindings/d1/database.ts:110-200packages/runtime-node/src/bindings/d1/object.ts:160-167src/runtime/fixtures/bindings/worker.js:150-153