Storage
R2
Use R2 bucket bindings with OpenOrange, backed by a local Rivet Actor or by an S3-compatible store.
Status: Supported. Runs on one Rivet Actor for each bucket, or on your S3-compatible store.
Configuration
OpenOrange reads the standard Wrangler R2 binding. No extra setup is needed:
{
"r2_buckets": [{ "binding": "BUCKET", "bucket_name": "uploads" }]
}
Like wrangler dev, OpenOrange names a local bucket by preview_bucket_name
when it is set, then by bucket_name. Bindings with the same bucket name share
data. OpenOrange ignores jurisdiction.
Store a bucket in S3 instead
By default, a Rivet Actor stores the bucket: on your machine under
orange dev, and on your Rivet cluster after orange deploy. To keep a bucket
in an S3-compatible store, set environment variables before you run
orange dev. The variable names contain the bucket name from above in
uppercase, with each character other than A-Z and 0-9 changed to _.
For bucket_name: "uploads":
| Variable | Required | Value |
|---|---|---|
ORANGE_R2_UPLOADS_S3_ENDPOINT | Yes | The S3 endpoint URL. See Choose a provider. Setting it turns on S3 for this bucket. |
ORANGE_R2_UPLOADS_S3_ACCESS_KEY_ID | Yes | The access key. Falls back to AWS_ACCESS_KEY_ID. |
ORANGE_R2_UPLOADS_S3_SECRET_ACCESS_KEY | Yes | The secret key. Falls back to AWS_SECRET_ACCESS_KEY. |
ORANGE_R2_UPLOADS_S3_BUCKET | No | The S3 bucket name. Defaults to bucket_name. |
ORANGE_R2_UPLOADS_S3_REGION | No | The S3 region. Falls back to AWS_REGION, then auto. |
# .dev.vars
ORANGE_R2_UPLOADS_S3_ENDPOINT=https://s3.us-east-1.amazonaws.com
ORANGE_R2_UPLOADS_S3_REGION=us-east-1
ORANGE_R2_UPLOADS_S3_ACCESS_KEY_ID=<access key ID>
ORANGE_R2_UPLOADS_S3_SECRET_ACCESS_KEY=<secret access key>
OpenOrange reads these variables from the shell environment, .dev.vars, or
.env. A value from the shell wins. The S3 bucket must already exist. If the endpoint is set
but a key is missing, the Worker does not start, and the error names the key
variables to set.
Choose a provider
| Provider | ..._S3_ENDPOINT | ..._S3_REGION | Keys |
|---|---|---|---|
| Amazon S3 | https://s3.<region>.amazonaws.com | The bucket’s region, such as us-east-1 | An IAM access key ID and secret access key |
| Cloudflare R2 | https://<account ID>.r2.cloudflarestorage.com | auto (the default) | The access key ID and secret access key of an R2 API token |
| Google Cloud Storage | https://storage.googleapis.com | auto (the default) | An HMAC key: the access ID, which starts with GOOG, and the secret |
| Backblaze B2 | https://s3.<region>.backblazeb2.com | The region in the endpoint, such as us-west-004 | An application key ID and application key |
| MinIO that you run | http://127.0.0.1:9000 | auto (the default) | The MinIO user and password. See Run MinIO locally. |
OpenOrange sends path-style requests, such as <endpoint>/<bucket>/<key>. A store
that accepts only virtual-hosted requests does not work. For example,
Tigris buckets created after
February 19, 2025 accept only virtual-hosted requests.
Run MinIO locally
export ORANGE_R2_UPLOADS_S3_ENDPOINT=http://127.0.0.1:9000
export ORANGE_R2_UPLOADS_S3_ACCESS_KEY_ID=minioadmin
export ORANGE_R2_UPLOADS_S3_SECRET_ACCESS_KEY=minioadmin
orange dev
How it works
The binding implements the R2 Workers API on top of one of two storage backends:
head(),get(),put(),delete()with one key or an array, andlist()withprefix,delimiter,cursor,startAfter,limit, andinclude.onlyIfas an object or asHeaders, andrangeas an object or asHeaders. A failed condition makesget()return the object without a body andput()returnnull.httpMetadata,customMetadata, and one ofmd5,sha1,sha256,sha384, orsha512onput(). A checksum that does not match fails the put and keeps the stored object.createMultipartUpload(),resumeMultipartUpload(), anduploadPart(),abort(), andcomplete()on the upload.R2Object.writeHttpMetadata(), andarrayBuffer(),bytes(),text(),json(), andblob()on a body.
Argument checks, limits, and error messages follow workerd and the local R2
backend of wrangler dev. For example, a key longer than 1024 bytes rejects
with put: The specified object name is not valid. (10020).
Default backend: a Rivet Actor per bucket
Each bucket 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 bucket is the actor openorange:r2-bucket with
the bucket name as its key, and the objects table lists the objects.
- A put cuts the body into 512 KiB chunks and writes each chunk with its own request. The object becomes visible only when a final request commits it, so a reader never sees a partial or mixed object.
- If the upload stops, for example because
orange devrestarts, the previous object stays visible. - Chunks of overwritten, deleted, and abandoned objects are deleted 5 minutes later by an alarm.
S3 backend
Each call becomes one or more S3 requests. OpenOrange evaluates onlyIf and
ranges with the same rules as the default backend. A conditional put reads the
current object first, then writes with If-Match or If-None-Match so that a
concurrent write makes the put return null. A put from a stream first writes
the body to a temporary file, because S3 needs the length before the upload
starts.
Conformance
An end-to-end orange dev test runs the same cases against both backends. The
S3 run uses a real MinIO server. The cases cover:
- Byte-equal round trips for 0 B, 1 B, 512 KiB ± 1 B, and 30 MiB, with MD5 ETags.
- Failed
onlyIfonget()andput(), as an object and asHeaders. - Ranges that cross a chunk boundary, and an unsatisfiable range.
- Two concurrent puts on one key while reads run.
- Checksum mismatches, metadata, listing, and multipart uploads.
- Invalid keys and
ssecKey.
The default backend also has a restart test. The S3 backend also has a test for an S3 endpoint that is down. The upstream workerd R2 tests use a mock R2 server that the conformance runner does not configure.
Limits and deviations
These apply to both backends:
ssecKeyis not supported yet. Calls with it reject with aNotSupportedError.- A put accepts a
ReadableStreamof any length. Cloudflare requires a known length. list()always honorsinclude, as with ther2_list_honor_includecompatibility flag.storageClassis stored as given and not checked.- There is no
orange r2command to manage objects from the CLI.
The default backend also has these limits:
- One Rivet Actor stores each bucket, so Rivet’s storage limit for one actor bounds the bucket. Cloudflare buckets have no size limit.
- Large objects are slow. On a development machine, a 30 MiB object takes about 1.5 seconds to write and 4.5 seconds to read.
- A read that takes longer than 5 minutes can fail if the object is overwritten or deleted during the read.
- Incomplete multipart uploads are kept until they are completed or aborted. Cloudflare aborts them after 7 days.
The S3 backend also has these limits:
versionis the S3 version ID when bucket versioning is on. Otherwise, it is the ETag.uploadedhas one-second precision.- S3 returns custom metadata keys in lowercase. Orange keeps the original
case of keys that have capital letters in one extra metadata entry,
orange-r2-key-case, and restores it on reads. The entry counts toward the S3 2 KB metadata limit, and other S3 clients see it. Two keys that differ only in case cannot both be stored. - S3 stores a default content type when the put has none.
checksumsreturns MD5 for single-part objects, plus SHA-1 or SHA-256 when the put gave one. A SHA-384 or SHA-512 checksum is verified but not stored.list()withincludereads each listed object’s metadata with one more request.- Completing a multipart upload with no parts fails. Aborting an unknown upload succeeds.
- Part ETags are the MD5 of the part.
- A conditional put is atomic only if the store supports
If-MatchandIf-None-Matchon writes. AWS S3, MinIO, and R2 support them.
Compatibility
From the latest conformance run. Scores on the compatibility page; every binding on the status page.
R2
r2_buckets Runs on Actor SQLite by default, or any S3-compatible store per bucket.
Pick the backend with ORANGE_R2_<BUCKET>_S3_* variables. On the default backend, one Rivet Actor stores each bucket, so Rivet's storage limit for one actor bounds the bucket. SSE-C keys throw.
Works with
Locally: Actor storage, MinIO
Any S3-compatible store, set per bucket with ORANGE_R2_<BUCKET>_S3_*.
Scales by S3 or bucket. With S3 set, as far as your S3 store goes. Without it, one actor per bucket.
Recorded results
WorksR2Available.On an S3-compatible store per bucket, or in actor SQLite when none is set. In actor SQLite, Rivet's storage limit for one actor bounds a bucket. SSE-C keys throw. 1/1 pass
- What to do
- In production, set
ORANGE_R2_<BUCKET>_S3_*. - Results
- example: r2 Pass
In the feature matrix
Alternativer2_buckets (r2_bucket)Reads and writes objects in an R2 bucket.Each bucket_name is an Orange-internal SQLite Durable Object on Rivet Actors that stores bodies in 512 KiB chunks. head(), get(), put(), delete(), list(), conditional and range requests, checksums, and multipart uploads work, and objects persist across orange dev restarts. A bucket can use an S3-compatible store instead through ORANGE_R2_<BUCKET>_S3_* variables. 1/2 pass · 7 files
- Notes
- On the default backend, one Rivet Actor stores each bucket, so Rivet's storage limit for one actor bounds the bucket. ssecKey throws NotSupportedError. Puts accept streams of any length. The S3 backend restores custom metadata key case from an orange-r2-key-case metadata entry. Upstream r2-test fails because the conformance runner drops workerd's r2Bucket binding. See docs/modules/r2.mdx.
- Tests
-
- api-tests--r2-test › r2-test AssertionError [ERR_ASSERTION]: Expected values to be loosely deep-equal: | | { Fail · Behavior differs
- api-tests--r2-write-http-metadata-validation-test › writeHttpMetadataValidation Pass
- Source
- src/config/load-wrangler.ts:813-820packages/runtime-node/src/runner.ts:526-539packages/runtime-node/src/bindings/r2/bucket.ts:166-294packages/runtime-node/src/bindings/r2/storage.ts:23-28packages/runtime-node/src/bindings/r2/storage.ts:192-737packages/runtime-node/src/bindings/r2/s3-backend.ts:302-627src/runtime/r2.test.ts:224-501