API reference

Overview

class Storage<Raw = unknown> extends ReadOnlyStorage {
  constructor(opts: { adapter: Adapter<Raw> });

  readonly raw: Raw;
  readonly snapshots: { create, list, head, delete, get };
  readonly forks:     { create, list, head, delete, get };

  // Reads — inherited from ReadOnlyStorage
  download(path, opts?): Promise<StorageItem | …>;
  head(path, opts?):     Promise<StorageItemMeta>;
  list(opts?):           Promise<ListResult>;
  url(path, opts?):      Promise<string>;

  // Writes — added by Storage
  upload(path, body, opts?): Promise<StorageItemMeta>;
  delete(path, opts?):       Promise<void>;
  copy(from, to, opts?):     Promise<void>;
  move(from, to, opts?):     Promise<void>;
  uploadUrl(path, opts?):    Promise<UploadUrlResult>;
}

storage.snapshots.get(id) returns a ReadOnlyStorage — same download overloads, no writes. storage.forks.get(name) returns a full Storage<Raw> so writes propagate and the typed raw survives fork navigation.

Every method that touches I/O accepts an optional signal?: AbortSignal. Cancelled operations throw StorageError({ code: 'Aborted' }) — see Get Started for the typed error catalogue.

upload()

storage.upload(
  path: string,
  body: BodyInput,
  opts?: UploadOptions,
): Promise<StorageItemMeta>;

type BodyInput =
  | string
  | Uint8Array
  | ArrayBuffer
  | Blob
  | ReadableStream<Uint8Array>;
await storage.upload('report.pdf', body, {
  contentType: 'application/pdf',
  metadata: { uploadedBy: 'alice' },
});

upload auto-decides multipart vs single PUT: ReadableStream is always multipart (size unknown upfront); size-known bodies (string, Uint8Array, Blob, ArrayBuffer) go multipart only above the threshold.

OptionTypeNotes
contentTypestringDefaults to application/octet-stream.
cacheControlstringCache-Control header on the stored object.
metadataRecord<string, string>User-defined headers. Vercel drops them; S3 lowercases keys.
multipartbooleanForce on/off. Default: auto-decide.
multipartThresholdnumberSize in bytes above which auto-decide picks multipart. Default 5 * 1024 * 1024.
partSize, concurrencynumberBackend-specific multipart tuning.
onProgress(e: { loaded, total }) => voidMultipart only.
signalAbortSignalCancels in-flight upload.

download()

Six overloads — the default returns the full StorageItem; the as: option returns just the body in the requested shape.

storage.download(path, opts?):                            Promise<StorageItem>;
storage.download(path, { as: 'stream', ... }):            Promise<ReadableStream<Uint8Array>>;
storage.download(path, { as: 'text',   ... }):            Promise<string>;
storage.download(path, { as: 'bytes',  ... }):            Promise<Uint8Array>;
storage.download(path, { as: 'blob',   ... }):            Promise<Blob>;
storage.download(path, { as: 'json',   ... }):            Promise<unknown>;
const item = await storage.download('photo.jpg');
// → { path, size, contentType, etag, lastModified, metadata, body: Uint8Array }

const text = await storage.download('readme.md',   { as: 'text' });
const bytes = await storage.download('blob.bin',   { as: 'bytes' });
const stream = await storage.download('video.mp4', { as: 'stream' });
const config = await storage.download('cfg.json',  { as: 'json' });

Byte-range reads via range: { offset, length } — matches HTTP Range semantics (extends-past-EOF returns the bytes that exist):

const slice = await storage.download('big.bin', {
  as: 'bytes',
  range: { offset: 4096, length: 1024 },
});
slice.byteLength; // 1024
OptionTypeNotes
as'stream' | 'text' | 'bytes' | 'blob' | 'json'Body shape. Omit for the full StorageItem.
range{ offset: number; length: number }Offset/length form to dodge end-bound ambiguity.
signalAbortSignal
storage.head(path: string, opts?: { signal? }): Promise<StorageItemMeta>;
const meta = await storage.head('photo.jpg');
meta.size;          // number
meta.contentType;   // string
meta.etag;          // string
meta.lastModified;  // Date
meta.metadata;      // Record<string, string> | undefined

Returns the same shape as one entry from list(). No body — call download() if you need the bytes.

list()

storage.list(opts?: ListOptions): Promise<ListResult>;

type ListResult = {
  items: StorageItemMeta[];
  cursor?: string;
};
const page = await storage.list({ prefix: 'reports/', limit: 100 });
for (const item of page.items) {
  console.log(item.path, item.size);
}
if (page.cursor) {
  const next = await storage.list({ prefix: 'reports/', cursor: page.cursor });
}
OptionTypeNotes
prefixstringFilter to keys under a path prefix.
limitnumberMax items returned per page. Backend caps apply.
cursorstringContinuation token from the previous response.
delimiterstringHierarchical listings (e.g. '/' for folder-like grouping).
signalAbortSignal

copy()

storage.copy(
  from: string,
  to: string,
  opts?: { signal?: AbortSignal },
): Promise<void>;
await storage.copy('inbox/photo.jpg', 'archive/2026/photo.jpg');

Server-side copy where the provider supports it; falls back to download-and-reupload otherwise. The destination overwrites silently if it exists.

move()

storage.move(
  from: string,
  to: string,
  opts?: { signal?: AbortSignal },
): Promise<void>;
await storage.move('tmp/upload-42.bin', 'final/upload-42.bin');

Implemented as copy then delete. Not atomic — if the delete fails after the copy succeeds, both keys exist.

delete()

storage.delete(path: string, opts?: { signal?: AbortSignal }): Promise<void>;
await storage.delete('old/report.pdf');

No-op if the key doesn’t exist on most providers (S3 semantics).

url()

storage.url(path: string, opts?: UrlOptions): Promise<string>;
// 5-minute GET URL.
const url = await storage.url('photo.jpg', { expiresIn: 300 });

// Force a download with a filename.
const dl = await storage.url('report.pdf', {
  expiresIn: 300,
  responseContentDisposition: 'attachment; filename="report.pdf"',
});
OptionTypeNotes
expiresInnumberSeconds. Default is provider-specific.
responseContentDispositionstringSet on the response (e.g. force-download).
responseContentTypestringOverride the response Content-Type.
signalAbortSignal

The fs adapter returns file:// URLs — explicitly not signed, local-dev only.

uploadUrl()

storage.uploadUrl(path: string, opts?: UploadUrlOptions): Promise<UploadUrlResult>;

type UploadUrlResult =
  | { method: 'PUT';  url: string; headers?: Record<string, string> }
  | { method: 'POST'; url: string; fields: Record<string, string> };

uploadUrl returns a discriminated value — PUT is the default; the adapter switches to POST (when supported) if you set maxSize / minSize so the provider can server-enforce the upload bounds.

// PUT — the client does fetch(url, { method: 'PUT', body }).
const put = await storage.uploadUrl('new.jpg', {
  expiresIn: 300,
  contentType: 'image/jpeg',
});
// → { method: 'PUT', url, headers? }

// POST — client submits multipart/form-data with `fields` + the file.
// The provider rejects uploads above `maxSize` or with the wrong type.
const post = await storage.uploadUrl('new.jpg', {
  expiresIn: 300,
  maxSize: 5_000_000,
  contentType: 'image/jpeg',
});
// → { method: 'POST', url, fields }
OptionTypeNotes
expiresInnumberSeconds.
contentTypestringLocks the request to a Content-Type.
maxSizenumberCap upload size in bytes. Switches to POST on adapters that support it.
minSizenumberMinimum upload size.
signalAbortSignal

See Adapters for which providers enforce maxSize / minSize server-side.

Snapshots

A snapshot is a frozen, read-only view of a bucket at a moment in time. Native on Tigris (zero-copy via createBucketSnapshot); emulated as sibling buckets/containers/folders on every other provider via server-side copy.

type SnapshotInfo = {
  readonly id: string;
  readonly name?: string;
  readonly createdAt: Date;
};

snapshots.create()

storage.snapshots.create(opts?: CreateSnapshotOptions): Promise<SnapshotInfo>;
const snap = await storage.snapshots.create({ name: 'pre-migration' });
snap.id;        // backend-generated, opaque
snap.name;      // 'pre-migration'
snap.createdAt; // Date

Captures the bucket’s current state. The id is always backend-generated (Tigris assigns one; copy-based adapters compose it as the parent name plus a -snapshot- separator and a 25-digit timestamp + random suffix). name is a human-readable label you provide.

OptionTypeNotes
namestringHuman-readable label. Stored on SnapshotInfo; useful for retrieval-by-name in your own code.
onProgress(e: { scanned, total? }) => voidCopy-based adapters fire this as they iterate keys. Tigris finishes in one call so it isn’t called.
signalAbortSignalCancels mid-copy on copy-based adapters; cancels the API call on Tigris.

snapshots.list()

storage.snapshots.list(): Promise<SnapshotInfo[]>;
const all = await storage.snapshots.list();
for (const snap of all) {
  console.log(snap.id, snap.name, snap.createdAt);
}

Returns every snapshot of the current bucket. No pagination — snapshot counts are expected to be small. No options.

snapshots.head()

storage.snapshots.head(id: string, opts?: { signal? }): Promise<SnapshotInfo>;
const meta = await storage.snapshots.head(snap.id);
meta.createdAt; // Date — useful for "snapshot is older than X" checks

Fetches a single snapshot’s metadata by id. Throws StorageError({ code: 'NotFound' }) if no snapshot with that id exists. Cheaper than list() when you already know the id.

snapshots.delete()

storage.snapshots.delete(id: string, opts?: { signal? }): Promise<void>;
await storage.snapshots.delete(snap.id);

Deletes a snapshot. On Tigris this is one call; on copy-based adapters it removes the sibling bucket/folder. Forks that were created from this snapshot keep their data — fromSnapshot on ForkInfo will still point at the deleted id, but reads against the fork itself are unaffected.

snapshots.get()

storage.snapshots.get(id: string): ReadOnlyStorage;
const reader = storage.snapshots.get(snap.id);

// Same overloaded download from ReadOnlyStorage.
const before = await reader.download('photo.jpg', { as: 'text' });
const meta   = await reader.head('photo.jpg');
const page   = await reader.list({ prefix: 'reports/' });
const url    = await reader.url('photo.jpg', { expiresIn: 300 });

Returns a ReadOnlyStorage bound to the snapshot — the same overloaded download, plus head, list, and url. No writes (the snapshot is frozen). Synchronous: this is just a handle; the lookup happens on the first read.

Forks

A fork is a writable branch of a bucket — your own isolated namespace, seeded from a snapshot or from the parent’s live state. Native on Tigris (zero-copy via createBucket({ sourceBucketName, sourceBucketSnapshot })); emulated as sibling buckets/containers/folders on every other provider.

type ForkInfo = {
  readonly name: string;
  readonly fromSnapshot?: string;
  readonly createdAt: Date;
};

forks.create()

storage.forks.create(opts: ForkOptions): Promise<ForkInfo>;
// Fork from a specific snapshot — reproducible.
await storage.forks.create({
  name: 'experiment-42',
  fromSnapshot: snap.id,
});

// Fork from the parent's current live state — seed-at-create.
await storage.forks.create({ name: 'experiment-43' });

Creates a writable fork. With fromSnapshot, the fork starts at exactly that frozen state. Without it, copy-based adapters copy the parent’s live state at the moment of creation; native adapters pass the omission through to their fork API.

OptionTypeNotes
namestringRequired. Must be unique within the parent. Used as the fork’s identifier in every other forks.* call.
fromSnapshotstringSnapshot id to seed from. Omit to seed from the parent’s live state.
onProgress(e: { copied, total, bytes? }) => voidCopy-based adapters fire this per object copied.
signalAbortSignalCancels mid-copy on copy-based adapters; cancels the API call on Tigris.

forks.list()

storage.forks.list(): Promise<ForkInfo[]>;
const all = await storage.forks.list();
for (const fork of all) {
  console.log(fork.name, fork.fromSnapshot, fork.createdAt);
}

Returns every fork of the current bucket. fromSnapshot is undefined for forks created from live state. No pagination, no options.

forks.head()

storage.forks.head(name: string, opts?: { signal? }): Promise<ForkInfo>;
const meta = await storage.forks.head('experiment-42');
meta.fromSnapshot; // id or undefined

Fetches a single fork’s metadata by name. Throws StorageError({ code: 'NotFound' }) if no fork with that name exists.

forks.delete()

storage.forks.delete(name: string, opts?: { signal? }): Promise<void>;
await storage.forks.delete('experiment-42');

Deletes the fork and its data. The parent is unaffected, and the snapshot the fork was seeded from (if any) is also unaffected.

forks.get()

storage.forks.get(name: string): Storage<Raw>;
const fork = storage.forks.get('experiment-42');

// Full read + write surface.
await fork.upload('config.json', JSON.stringify({ flag: true }));
const cfg = await fork.download('config.json', { as: 'json' });

// Nested forks: same typed `raw` propagates through.
const nested = fork.forks.get('sub-experiment');
nested.raw; // same Raw type as storage.raw

Returns a full Storage<Raw> bound to the fork — every read and write method, plus snapshots and forks of its own. The Raw generic survives navigation, so storage.raw and storage.forks.get('x').raw are the same client type without a cast.