---
title: "@playstack/storage"
description: "Runtime-neutral object storage with streaming, conditional writes, listing, copy, ranges, multipart upload, and integrity checks."
tags: ["package","infrastructure","storage","s3","r2","free"]
---

{/* package-access:start */}

> **Free.** MIT licensed. Check preview availability before installing. See [package access](/docs/packages#access-policy).

{/* package-access:end */}

`@playstack/storage` provides one runtime-neutral object boundary across Cloudflare R2, S3-compatible services, local filesystem, and in-memory testing providers.

{/* package-install:start */}

## Install

After confirming [preview access](/docs/packages#access-policy), install the package at your application's shared Playstack version:

```sh
npm install --save-exact @playstack/storage@0.1.0-beta.1
```

Check the peer requirements below before choosing a runtime or provider.

{/* package-install:end */}

## Compose and store objects

For a complete example without cloud credentials, start with the [storage quickstart](/docs/getting-started#your-first-working-example).

**Workers composition example:** the snippet below assumes `env.ASSETS` is your configured R2 bucket binding, `pdfBytes` contains the document, and `signer` is your `StorageUrlSigner`. Omit `signer` if you do not need signed URLs. In Node, choose the S3 or filesystem entry point instead of a Workers binding.

```ts
import { createStorage } from '@playstack/storage'
import { R2StorageProvider } from '@playstack/storage/r2'

const storage = createStorage({
  driver: new R2StorageProvider(env.ASSETS, {
    publicBaseUrl: 'https://assets.example.com',
    signer,
  }),
  prefix: 'billing',
})

const stored = await storage.put('invoices/invoice-1.pdf', pdfBytes, {
  contentType: 'application/pdf',
  cacheControl: 'private, max-age=0',
  metadata: { account_id: 'account-1' },
})
```

Bodies may be strings, bytes, ArrayBuffer, Blob, or Web readable streams. Reads always return a Web stream. Prefix and logical keys are canonical relative paths; dot/empty segments, backslashes, controls, and paths over 1,024 UTF-8 bytes are rejected.

## Put and URL properties

### Common method results

| Call | Result | Missing object / important behavior |
| --- | --- | --- |
| `put(key, body, options?)` | `Promise<StorageObjectMetadata>` | Creates or replaces an object; use write preconditions when replacement is not allowed. |
| `get(key, options?)` | `Promise<StorageObject \| null>` | Returns `null` when absent; consume or cancel the returned body stream. |
| `head(key, options?)` | `Promise<StorageObjectMetadata \| null>` | Metadata only, without downloading the body. |
| `exists(key, options?)` | `Promise<boolean>` | Returns whether the object is present. |
| `delete(key, options?)` | `Promise<void>` | Deletes one key, not an application record or tenant. |
| `list(options?)` | `Promise<StorageListPage>` | A bounded page, not an unbounded list of the bucket. |
| `verify(key, expected, options?)` | `Promise<StorageObjectMetadata \| null>` | Returns `null` when absent; throws on an integrity mismatch. |
| `url(key)` | `string` | Requires public-URL capability; does not grant authorization. |
| `signedUrl(key, options)` | `Promise<string>` | Requires signing capability; treat the result as a secret capability. |

For all overloads and option fields, open the `StorageDriver` and `StoragePutOptions` declarations through your editor. The entry-point table below identifies their installed files.

| Surface                | Properties                                                                                       |
| ---------------------- | ------------------------------------------------------------------------------------------------ |
| `CreateStorageOptions` | Required provider plus optional canonical prefix.                                                |
| Put options            | Content type/encoding, cache control, disposition, length, and string metadata.                  |
| Signed URL             | `get` or `put`, expiry from 1 second through 7 days, optional disposition, and PUT content type. |
| Capabilities           | Independent public URL, signed GET, and signed PUT booleans.                                     |

Calling an unsupported URL operation throws `StorageCapabilityError`. When a signed PUT includes content type or disposition, the uploader must send the exact signed header.

## Error handling

Errors are exported from `@playstack/storage/errors`. A missing read is `null`, not an exception. The main error codes are:

| Error | Code | What to do |
| --- | --- | --- |
| `StorageConfigurationError` | `playstack/storage/invalid_configuration` | Correct the provider configuration. |
| `StorageInputError` | `playstack/storage/invalid_input` | Correct the key, options or body. |
| `StorageCapabilityError` | `playstack/storage/unsupported_capability` | Choose a provider that supports the operation. |
| `StoragePreconditionError` | `playstack/storage/precondition_failed` | Reconcile the object state; do not overwrite blindly. |
| `StorageAbortedError` | `playstack/storage/operation_aborted` | Treat the operation as cancelled. |
| `StorageIntegrityError` | `playstack/storage/integrity_failed` | Reject the bytes and investigate the mismatch. |
| `StorageUnavailableError` | `playstack/storage/unavailable` | Consider a bounded retry only when repeating the operation is safe. |

The first six are non-retryable. `StorageUnavailableError` is retryable, but that flag does not establish whether a write took effect. Your application owns write idempotency and recovery. Do not expose provider exception details or signed URLs to end users.

## Advanced portable operations

```ts
const page = await storage.list({ prefix: 'fonts', limit: 100 })
await storage.copy('fonts/source.woff2', 'fonts/archive/source.woff2')
await storage.deletePrefix({ prefix: 'temporary', maxObjects: 1_000 })

const chunk = await storage.getRange('packages/font.tgz', {
  start: 0,
  end: 1_048_575,
})

await storage.verify('packages/font.tgz', {
  size: expectedSize,
  etag: expectedEtag,
  sha256: expectedSha256,
})
```

Listing and prefix deletion are cursor-based and bounded. Range reads retain the complete object size. Verification can use size and ETag without downloading the body or SHA-256 when complete-body integrity is required. Operations accept an `AbortSignal` only when the selected provider declares that capability.

S3, R2, and memory providers support atomic create-only and compare-and-swap writes through `ifNoneMatch` and `ifMatch`. S3 also exposes portable multipart create, upload-part, complete, and abort operations. Unsupported capabilities fail explicitly before I/O.

## Provider bridge

```ts
interface StorageProvider<TClient> {
  client: TClient
  capabilities: StorageCapabilities
  put(key, body, options): Promise<StorageObjectMetadata>
  get(key): Promise<StorageObject | null>
  head(key): Promise<StorageObjectMetadata | null>
  delete(key): Promise<void>
  url(key): string
  signedUrl(key, options): Promise<string>
}
```

The driver wrapper adds logical prefixing, validation, normalized errors, `exists`, and stable metadata while exposing the exact native client.

## Provider-specific composition

- R2 accepts a structural bucket, optional public base URL, and optional external `StorageUrlSigner` for S3-compatible presigned URLs.
- S3 accepts AWS client configuration plus bucket/public URL options and uses optional AWS SDK peers.
- Filesystem accepts a non-root directory plus optional public URL, signer, byte ceiling, clock, and metadata sidecar root. Writes use temp-file rename.
- Memory storage and the shared provider contract suite live under `@playstack/storage/testing`.
- `@playstack/nest-storage` registers one or more named stores with stable injection tokens while preserving native clients and application lifecycle ownership.

## Boundary

Storage does not decide tenant organization, media processing, access policy, lifecycle rules, or bucket administration. The application owns clients, credentials, shutdown, and domain-to-key mapping; use `storage.client` for provider-specific operations outside the portable surface.

## Bounded integrity verification

SHA-256 verification now defaults to a **16 MiB actual-byte ceiling**. Set `verify(key, expected, { maxBytes, signal })` deliberately for larger objects. HEAD metadata permits early rejection but is not trusted instead of counting downloaded bytes; truncation, overflow and cancellation are checked. Changed/abandoned streams are cancelled. The portable hasher buffers up to the bound, so it is not constant-memory.

Node services can inject `storageSha256Node` from `@playstack/storage/node` through `createStorage({ driver, sha256: storageSha256Node })` for incremental hashing under the same byte/abort contract. Use the S3 adapter for MinIO and R2's S3 endpoint in Node; the structural R2 binding is for Workers. Verification proves the downloaded snapshot, not immutability of future reads. Retain product key layouts, authorization and deployment-specific integrity tests.

{/* package-reference:start */}

## API entry points and requirements

Reference snapshot: `@playstack/storage@0.1.0-beta.1`. Import only the entry point your runtime needs. Paths below are relative to the installed package; use **Go to Definition** in your editor to inspect exact parameters, return types and overloads. Do not import the declaration-file paths directly.

| Public entry point | Declaration file |
| --- | --- |
| `@playstack/storage` | `./dist/index.d.ts` |
| `@playstack/storage/errors` | `./dist/errors.d.ts` |
| `@playstack/storage/types` | `./dist/types.d.ts` |
| `@playstack/storage/testing` | `./dist/testing.d.ts` |
| `@playstack/storage/r2` | `./dist/r2.d.ts` |
| `@playstack/storage/s3` | `./dist/s3.d.ts` |
| `@playstack/storage/filesystem` | `./dist/filesystem.d.ts` |
| `@playstack/storage/node` | `./dist/node.d.ts` |
| `@playstack/storage/playstack.integration.json` | No TypeScript declaration (asset or metadata export). |
| `@playstack/storage/package.json` | No TypeScript declaration (asset or metadata export). |

Node.js engine requirement: `>=20`. This is not a claim that every entry point works in browsers or Workers.

### Peer dependencies

Keep existing framework versions that satisfy these ranges. Install optional peers only when using the corresponding adapter. The package manager resolves ordinary dependencies separately.

| Peer | Compatible range | When needed |
| --- | --- | --- |
| `@aws-sdk/client-s3` | `^3.1116.0` | Optional; only for the entry points that use it. |
| `@aws-sdk/s3-request-presigner` | `^3.1116.0` | Optional; only for the entry points that use it. |

For a complete first program, start with [Getting started](/docs/getting-started). For API lookup and partial-example conventions, see [Reading the reference](/docs/packages#reading-the-reference). Provider failures, lifecycle requirements and application responsibilities remain described in the guide above; types alone do not establish production safety.

{/* package-reference:end */}
