> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ooneex.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Cloudflare Storage

> Production object storage on Cloudflare R2 behind the framework's IStorage interface.

`CloudflareStorage` is the [Storage](/components/storage) component's [Cloudflare R2](https://developers.cloudflare.com/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`; `CloudflareStorage` only supplies the connection options.
* **Same `IStorage` interface.** `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 and `getOptions()`.
* **Flexible content.** `put()` accepts strings, `ArrayBuffer`, `Blob`, `Request`/`Response`, and `BunFile`/`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.

```bash theme={null}
bun add @ooneex/storage
```

## 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`.                                             |

```bash theme={null}
STORAGE_CLOUDFLARE_ACCESS_KEY=your_access_key
STORAGE_CLOUDFLARE_SECRET_KEY=your_secret_key
STORAGE_CLOUDFLARE_ENDPOINT=https://your-account.r2.cloudflarestorage.com
STORAGE_CLOUDFLARE_REGION=EEUR
```

All three required values are validated when `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.

```typescript theme={null}
import { container } from "@ooneex/container";
import { CloudflareStorage } from "@ooneex/storage";

const storage = container.get(CloudflareStorage).setBucket("uploads");

// Write content — returns the number of bytes written
await storage.put("avatars/123.png", file); // file: Blob | BunFile | ArrayBuffer | ...
await storage.put("config.json", JSON.stringify({ theme: "dark" }));

// Read it back
const config = await storage.getAsJson<{ theme: string }>("config.json");
const buffer = await storage.getAsArrayBuffer("avatars/123.png");

// Existence, listing, and removal
await storage.exists("avatars/123.png"); // true
const keys = await storage.list();
await storage.delete("avatars/123.png");
```

Upload a local file or a whole directory, and stream large files instead of buffering them:

```typescript theme={null}
// Upload one local file, or a directory recursively (images only)
await storage.putFile("avatars/123.png", "/tmp/avatar.png");
await storage.putDir("avatars", {
  path: "/tmp/avatars",
  filter: /\.(png|jpe?g)$/,
});

// Stream a large object straight to the response
const stream = storage.getAsStream("videos/intro.mp4");
return new Response(stream);
```

### Custom buckets

Register a named storage under your own class so each bucket has a dedicated, injectable type:

```typescript theme={null}
import { decorator } from "@ooneex/storage";
import { CloudflareStorage } from "@ooneex/storage";

@decorator.storage()
export class UploadStorage extends CloudflareStorage {}
```

```typescript theme={null}
import { inject } from "@ooneex/container";

export class UploadService {
  constructor(@inject(UploadStorage) private readonly storage: UploadStorage) {}
}
```

## 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.

```bash theme={null}
bun add @ooneex/app @ooneex/storage
```

Inject it into a service or controller, set the bucket, and use the `IStorage` methods:

```typescript theme={null}
import { inject } from "@ooneex/container";
import { CloudflareStorage } from "@ooneex/storage";

export class MediaService {
  constructor(
    @inject(CloudflareStorage) private readonly storage: CloudflareStorage,
  ) {}

  public async uploadAvatar(userId: string, file: Blob): Promise<void> {
    await this.storage.setBucket("uploads").put(`avatars/${userId}.png`, file);
  }
}
```

To bind a dedicated R2 bucket to its own injectable type, register a subclass with `@decorator.storage()`:

```typescript theme={null}
import { decorator } from "@ooneex/storage";
import { CloudflareStorage } from "@ooneex/storage";

@decorator.storage()
export class AvatarStorage extends CloudflareStorage {}
```

Set `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.   |

```typescript theme={null}
import { StorageException } from "@ooneex/storage";

try {
  await storage.put("avatars/123.png", file);
} catch (error) {
  if (error instanceof StorageException) {
    logger.error(`Storage error [${error.key}]: ${error.message}`, error.data);
  }
  throw error;
}
```

## 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_ENDPOINT` at your account's R2 S3 endpoint, not the public bucket URL.
* **Use stable, namespaced keys.** Group objects under prefixes like `avatars/123.png` so `list()` and `clearBucket()` stay predictable.
* **Stream large files.** Prefer `getAsStream()` and `putFile`/`putDir` over 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 `CloudflareStorage` subclass for each bucket so it can be injected with its own type.

See the [Storage component](/components/storage) for the full interface and the other backends, including [Bunny Storage](/integrations/bunny-storage).
