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

# Storage

> The in-process storage provider abstraction: one StorageProvider contract with local, R2, and S3-compatible backends, and how it relates to managed Cloud storage.

Modules sometimes need to put bytes somewhere: an uploaded document, a rendered invoice PDF, a media asset. Voyant handles that with the `@voyant-travel/storage` package, a small provider abstraction that gives portable code one contract and lets the deployment choose the backend.

This page covers the platform storage provider interface and its built-in backends. It is the in-process abstraction you wire into a self-hosted deployment. It is distinct from the managed [Cloud storage](/docs/services/storage) service, which Voyant runs for you. Cross-link below.

## One provider contract

Everything in the package targets a single interface, `StorageProvider`, from `@voyant-travel/storage/types`:

```ts theme={null}
interface StorageProvider {
  readonly name: string
  upload(body: StorageUploadBody, options?: UploadOptions): Promise<StorageObject>
  delete(key: string): Promise<void>
  signedUrl(key: string, expiresIn: number): Promise<string>
  get(key: string): Promise<ArrayBuffer | null>
}
```

The contract is deliberately narrow: upload bytes, delete by key, mint a time-limited URL, fetch bytes back. `StorageUploadBody` accepts an `ArrayBuffer`, a `Uint8Array`, or a `Blob`. `upload` returns a `StorageObject` carrying the object `key` and a public `url` (an empty string when the object is private and only reachable through `signedUrl`). `get` returns `null` when the key is absent.

Portable module code targets this interface. The backend is a deployment choice, not a fork of the module.

## The service wrapper

Most deployments use exactly one storage backend, so `createStorageService(provider)` from `@voyant-travel/storage/service` wraps a single provider as a named `StorageService`:

```ts theme={null}
import { createStorageService } from "@voyant-travel/storage"
import { createS3CompatibleStorageProvider } from "@voyant-travel/storage/providers/s3-compatible"

const storage = createStorageService(
  createS3CompatibleStorageProvider({ bucket: env.MEDIA_BUCKET, publicBaseUrl: "https://cdn.example.com/" }),
)

const { key } = await storage.upload(bytes, { contentType: "image/png" })
const url = await storage.signedUrl(key, 300)
```

The service exposes the same `upload` / `delete` / `signedUrl` / `get` surface as the provider, plus a `provider` reference. It is a thin convenience, not an orchestration layer.

## Built-in backends

The package ships three providers. Each is created by a factory and satisfies the same `StorageProvider` contract.

| Provider  | Factory                             | Subpath                                          | Use for                                                   |
| --------- | ----------------------------------- | ------------------------------------------------ | --------------------------------------------------------- |
| **Local** | `createLocalStorageProvider`        | `@voyant-travel/storage/providers/local`         | Unit tests and local runs. In-memory `Map`, lost on exit. |
| **R2**    | `createS3CompatibleStorageProvider` | `@voyant-travel/storage/providers/s3-compatible` | Cloudflare deployments. Binds to an R2 bucket binding.    |
| **S3**    | `createS3CompatibleStorageProvider` | `@voyant-travel/storage/providers/s3`            | AWS S3 and S3-compatible services via SigV4.              |

### Local

`createLocalStorageProvider()` keeps objects in an in-memory `Map` held in a closure. It is the right backend for unit tests and for running workflows locally without touching remote storage. Data does not survive the process.

### R2

`createS3CompatibleStorageProvider({ bucket })` binds to a Cloudflare R2 bucket binding (for example `env.MEDIA_BUCKET`). The binding handles authentication at the Worker runtime boundary, so no credentials live in this layer.

R2 bindings do not mint signed URLs by themselves. The R2 provider adds a `publicUrl(key)` method for permanent public URLs (requires `publicBaseUrl`, a public custom domain or a Worker route that proxies the bucket). For time-limited access, configure a `signer`.

<Warning>
  Calling `signedUrl` on the R2 provider without a configured `signer` throws. This is intentional: falling back to `${publicBaseUrl}${key}` would return a permanent, unauthenticated URL while the caller believes it expires after `expiresIn` seconds. Use `publicUrl(key)` when a permanent URL is what you actually want.
</Warning>

### S3 and S3-compatible

`createS3CompatibleStorageProvider({ region, bucket, accessKeyId, secretAccessKey })` signs every request with AWS SigV4 using Web Crypto, so it needs no AWS SDK. It accepts an optional `sessionToken` for temporary credentials.

For S3-compatible services (MinIO, Backblaze B2, DigitalOcean Spaces, Wasabi, or R2's S3 API), set a custom `endpoint`. `forcePathStyle` defaults to `true` for the widest compatibility; set it to `false` for virtual-hosted-style URLs. `signedUrl` presigns a GET URL with SigV4.

The SigV4 signing primitives, `signRequest` and `presignUrl`, are also exported directly from `@voyant-travel/storage/lib/sigv4` for advanced use. They are verified against the AWS canonical test vectors.

## Package exports

| Subpath                                          | Contents                                                                  |
| ------------------------------------------------ | ------------------------------------------------------------------------- |
| `.`                                              | Barrel re-exporting everything below.                                     |
| `@voyant-travel/storage/types`                   | `StorageProvider`, `StorageObject`, `StorageUploadBody`, `UploadOptions`. |
| `@voyant-travel/storage/service`                 | `createStorageService`, `StorageService`, `StorageError`.                 |
| `@voyant-travel/storage/providers/local`         | `createLocalStorageProvider`.                                             |
| `@voyant-travel/storage/providers/s3-compatible` | `createS3CompatibleStorageProvider`.                                      |
| `@voyant-travel/storage/providers/s3`            | `createS3CompatibleStorageProvider`.                                      |
| `@voyant-travel/storage/lib/sigv4`               | `signRequest`, `presignUrl`.                                              |

## Framework storage versus Cloud storage

This package is the in-process abstraction you assemble into a deployment yourself: you pick a provider, supply the bucket or credentials, and own the operational surface.

[Cloud storage](/docs/services/storage) is the managed alternative. Voyant runs the backend, provisioning, and access for you, so you do not wire up an R2 binding or carry S3 credentials. Reach for the platform package when you self-host and want to choose and control the backend. Reach for Cloud storage when you want it managed.

## Next steps

<CardGroup cols={2}>
  <Card title="Cloud storage" icon="cloud" href="/docs/services/storage">
    The managed storage service, the counterpart to this in-process abstraction.
  </Card>

  <Card title="Configuration" icon="sliders" href="/docs/platform/fundamentals/configuration">
    How a deployment chooses and wires its storage backend.
  </Card>

  <Card title="Services" icon="gears" href="/docs/concepts/how-it-is-built">
    Where module code calls into a storage provider.
  </Card>

  <Card title="Glossary" icon="book-open" href="/docs/concepts/glossary">
    The shared vocabulary behind the storage model.
  </Card>
</CardGroup>
