CloudflareStorage is the Storage component’s Cloudflare R2 backend. It extends the shared Storage base class, which talks to R2 through Bun’s built-in S3 client — R2 is S3-compatible — so you get production object storage with zero egress fees behind the same IStorage interface as every other backend. Switch to or from R2 without changing a single call site.
Why Cloudflare R2
- S3-compatible. Backed by Bun’s native
S3Client;CloudflareStorageonly supplies the connection options. - Same
IStorageinterface.put,getFile,getAsJson,delete,list, and more — identical to every other storage backend. - Shared base class. Inherits all transport logic from
Storage; the subclass just provides credentials andgetOptions(). - Flexible content.
put()accepts strings,ArrayBuffer,Blob,Request/Response, andBunFile/S3File. - Container-managed. Registered with
@decorator.storage()and resolved from the container.
Installation
CloudflareStorage ships with @ooneex/storage. It uses Bun’s built-in S3 client, so there’s no extra SDK to install.
Environment variables
| Variable | Required | Purpose |
|---|---|---|
STORAGE_CLOUDFLARE_ACCESS_KEY | Yes | R2 access key. Missing throws StorageException (CONFIG_REQUIRED). |
STORAGE_CLOUDFLARE_SECRET_KEY | Yes | R2 secret key. Missing throws StorageException (CONFIG_REQUIRED). |
STORAGE_CLOUDFLARE_ENDPOINT | Yes | R2 S3 endpoint URL. Missing throws StorageException (CONFIG_REQUIRED). |
STORAGE_CLOUDFLARE_REGION | No | R2 region. Defaults to EEUR. |
CloudflareStorage is constructed, so missing credentials fail fast at startup.
How it works
CloudflareStorage reads its credentials, then getOptions() returns the S3Options the base Storage class uses to build a Bun.S3Client. Calling setBucket() constructs (or rebuilds) the client for that bucket; from then on every read and write goes through the S3 client. The active bucket is the R2 bucket the client targets.
| Method | Purpose |
|---|---|
getBucket() / setBucket(name) | Read or switch the active R2 bucket; setBucket chains. |
list() | List the keys in the current bucket. |
clearBucket() | Delete every object in the current bucket. |
exists(key) | Whether an object exists. |
delete(key) | Remove one object. |
put(key, content) | Write content; returns the number of bytes written. |
putFile(key, localPath) | Upload a local file. |
putDir(bucket, options) | Upload a local directory recursively with an optional filter regex. |
getFile(key, options) | Download an object to options.outputDir (optionally renamed). |
getAsJson<T>(key) | Parse the object as JSON. |
getAsArrayBuffer(key) | Read the object as an ArrayBuffer. |
getAsStream(key) | Read the object as a ReadableStream. |
Usage
The API is the same as every other storage backend.Custom buckets
Register a named storage under your own class so each bucket has a dedicated, injectable type:Use in the app
In an@ooneex/app application, storage isn’t a dedicated App config slot — CloudflareStorage registers itself with the container through @decorator.storage() as soon as the class is imported, and you inject it wherever you read or write files.
IStorage methods:
@decorator.storage():
STORAGE_CLOUDFLARE_ACCESS_KEY, STORAGE_CLOUDFLARE_SECRET_KEY, STORAGE_CLOUDFLARE_ENDPOINT, and (optionally) STORAGE_CLOUDFLARE_REGION in your .env.yml (or environment) so the backend can connect.
Exceptions
CloudflareStorage throws StorageException on misconfiguration or a failed operation, carrying a machine-readable key.
| Key | When |
|---|---|
CONFIG_REQUIRED | Missing the access key, secret key, or endpoint. |
STORAGE_UPLOAD_FAILED | A write failed. |
STORAGE_DOWNLOAD_FAILED | A read or download failed. |
STORAGE_LIST_FAILED | Listing a bucket failed. |
FILE_NOT_FOUND | A read targeted an object that does not exist. |
Best practices
- Set the bucket first. Call
setBucket()before reads or writes — it also builds the S3 client for that bucket. - Use the right endpoint. Point
STORAGE_CLOUDFLARE_ENDPOINTat your account’s R2 S3 endpoint, not the public bucket URL. - Use stable, namespaced keys. Group objects under prefixes like
avatars/123.pngsolist()andclearBucket()stay predictable. - Stream large files. Prefer
getAsStream()andputFile/putDirover loading whole files into memory. - Keep credentials in the environment. Load the access key, secret key, and endpoint from
.env; never hard-code them. - Subclass per bucket. Register a
CloudflareStoragesubclass for each bucket so it can be injected with its own type.