@playstack/storage
Runtime-neutral object storage with streaming, conditional writes, listing, copy, ranges, multipart upload, and integrity checks.
Free. MIT licensed. Check preview availability before installing. See package access.
@playstack/storage provides one runtime-neutral object boundary across Cloudflare R2, S3-compatible services, local filesystem, and in-memory testing providers.
Install
After confirming preview access, install the package at your application's shared Playstack version:
npm install --save-exact @playstack/storage@0.1.0-beta.1Check the peer requirements below before choosing a runtime or provider.
Compose and store objects
For a complete example without cloud credentials, start with the storage quickstart.
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.
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
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
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
StorageUrlSignerfor 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-storageregisters 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.
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. For API lookup and partial-example conventions, see Reading the reference. Provider failures, lifecycle requirements and application responsibilities remain described in the guide above; types alone do not establish production safety.