Tanstack Start

Server client

new S3Delivery() and every method it exposes.

The S3Delivery class is the server-side client. It holds your secret API key, so use it only on a server — in a route handler, a background job, or an AI tool. Never ship it to the browser.

import { S3Delivery } from "s3delivery";

const s3 = new S3Delivery(); // reads S3DELIVERY_TOKEN

Constructor

new S3Delivery(options?: {
  token?: string;
  apiUrl?: string;
  fetch?: typeof fetch;
});
OptionDefaultNotes
tokenS3DELIVERY_TOKEN env varYour secret API key (s3d_live_…). Throws MISSING_TOKEN if absent.
apiUrlS3DELIVERY_API_URL env var, else the bundled defaultOverride the API base URL.
fetchglobal fetchCustom fetch implementation (for testing or non-standard runtimes).

If no token is passed and S3DELIVERY_TOKEN is not set, the constructor throws an S3DeliveryError with code MISSING_TOKEN.

Methods at a glance

MethodDescription
uploadEnd-to-end upload: reserve → bytes straight to bucket → confirm. Returns the file.
createUploadLow-level: reserve a record and mint presigned upload URL(s).
completeLow-level: confirm an upload finished.
listList your files, newest first.
getGet a fresh signed URL plus metadata.
downloadFetch the raw bytes as a Blob.
existstrue / false — never throws on a missing or deleted file.
updateRename a file or change its visibility.
deleteDelete the object and soft-delete the record.
usageStorage used, file count, plan name, and plan limits.
const file = await s3.upload(blob, { name: "report.pdf", visibility: "private" });
const { url } = await s3.get(file.id);
const bytes = await s3.download(file.id); // Blob
if (await s3.exists(file.id)) await s3.delete(file.id);

upload

The hero method. Reserves a record, sends the bytes directly to your bucket, and confirms — all in one call.

upload(
  file: Blob & { name?: string },
  opts?: {
    name?: string;
    contentType?: string;
    visibility?: "private" | "public" | "unlisted";
    // plus upload-engine options: onProgress, retries, retryDelay,
    // maxRetryDelay, concurrency, signal
  },
): Promise<S3DeliveryFile>;
ParamDefaultNotes
fileA Blob, File, or any Blob-like value.
opts.namefile.name, else "file"Filename to store.
opts.contentTypefile.type, else application/octet-streamMIME type.
opts.visibility"private""private", "unlisted", or "public".
opts.onProgress({ loaded, total, percent }) => void.

It accepts the same retry, concurrency, and signal options as the upload engine, so large server uploads get parallel multipart and automatic retry for free.

const file = await s3.upload(blob, {
  name: "report.pdf",
  visibility: "private",
  onProgress: (p) => console.log(`${p.percent}%`),
});

console.log(file.id, file.url);

createUpload

Low-level. Reserves a file record and returns presigned upload URL(s) without sending any bytes. Most code should call upload instead; use this when you broker the browser handshake yourself (this is what createRouteHandler calls internally).

createUpload(input: {
  filename: string;
  contentType?: string;
  size: number;
  visibility?: "private" | "public" | "unlisted";
}): Promise<{
  file: S3DeliveryFile;
  upload:
    | { mode: "single"; url: string; method: "PUT" }
    | { mode: "multipart"; uploadId: string; partSize: number; parts: { partNumber: number; url: string }[] };
}>;

The returned upload descriptor is single for files under 100 MB and multipart (with one URL per 16 MB part) at or above that. Pass the descriptor to uploadToPresigned to send the bytes.

complete

Low-level. Confirms an upload finished and flips the record to ready. Pairs with createUpload.

complete(
  fileId: string,
  body?: { uploadId?: string; parts?: { partNumber: number; etag: string }[] },
): Promise<S3DeliveryFile>;

For a multipart upload, pass the uploadId from the descriptor and the collected part ETags. For a single upload, call it with no body. The API verifies the object exists in the bucket before marking the file ready — if the PUT never landed, complete throws.

list

Lists your ready files, newest first.

list(opts?: { limit?: number; offset?: number }): Promise<{
  files: S3DeliveryFile[];
  limit: number;
  offset: number;
}>;
ParamNotes
limitMax files to return. Server default 50, max 100.
offsetNumber of files to skip, for pagination.
const { files } = await s3.list({ limit: 20, offset: 0 });

get

Returns a freshly-signed, short-lived URL plus the file's metadata.

get(
  fileId: string,
  opts?: { download?: boolean },
): Promise<{ url: string; file: S3DeliveryFile }>;

Set download: true to get an attachment URL (Content-Disposition: attachment) instead of an inline one. The signed url expires after 15 minutes for private files and 24 hours for public/unlisted files; the stable file.url (/f/:id) re-signs on every request, so store that for long-lived links.

const { url, file } = await s3.get(fileId, { download: true });

download

Signs a fresh URL and fetches the bytes directly from the bucket, returning a Blob. The bytes never pass through the API.

download(fileId: string): Promise<Blob>;
const blob = await s3.download(fileId);
const buffer = Buffer.from(await blob.arrayBuffer());

Throws DOWNLOAD_FAILED if the signed URL cannot be fetched.

exists

Returns whether a file exists and is not deleted — without throwing on a 404.

exists(fileId: string): Promise<boolean>;
if (await s3.exists(fileId)) {
  // safe to link / serve
}

Other errors (network, auth) still propagate.

update

Renames a file or changes its visibility.

update(
  fileId: string,
  patch: { filename?: string; visibility?: "private" | "public" | "unlisted" },
): Promise<S3DeliveryFile>;
await s3.update(fileId, { visibility: "public" });

See Access control for what each visibility level means.

delete

Removes the object from your bucket and soft-deletes the record. Irreversible.

delete(fileId: string): Promise<{ id: string; deleted: true }>;
await s3.delete(fileId);

usage

Reports storage used, file count, the plan name, and the plan's limits.

usage(): Promise<{
  usedBytes: number;
  fileCount: number;
  plan: string;
  limits: { maxFileBytes: number; maxTotalBytes: number; maxApiKeys: number };
}>;
const { usedBytes, limits } = await s3.usage();
const remaining = limits.maxTotalBytes - usedBytes;

See Quotas & plan limits for the current numbers.

The S3DeliveryFile shape

Every file-returning method resolves to this object:

interface S3DeliveryFile {
  id: string;
  name: string;
  contentType: string;
  size: number;
  status: "pending" | "ready" | "deleted";
  visibility: "private" | "public" | "unlisted";
  /** Stable URL that redirects to a freshly-signed bucket URL. */
  url: string;
  createdAt: string; // ISO 8601
  uploadedAt: string | null; // ISO 8601, set on completion
}

On this page