Latest update — v2.0.0 released

Write once. Store anywhere.

A unified SDK for S3, R2, GCS, Azure, and every other object or blob store. One small API, web standards, and an escape hatch when you need the native client.

Read the docs
Azure Blob StorageBoxDigitalOceanDropboxGoogle Cloud StorageGoogleDriveMinIONetlify BlobsOneDriveR2S3SupabaseUploadThingVercelAzure Blob StorageBoxDigitalOceanDropboxGoogle Cloud StorageGoogleDriveMinIONetlify BlobsOneDriveR2S3SupabaseUploadThingVercel

and 31 more — see every adapter →

Live snippet

The exact same code. Any backend.

Switch the adapter, keep every call site. Here's the same upload, download, head, list, and delete sequence across five providers.

import { Files } from "files-sdk";
import { s3 } from "files-sdk/s3";

const files = new Files({
  adapter: s3({ bucket: "uploads", region: "us-east-1" }),
});

await files.upload("hello.txt", "world");
const file = await files.download("hello.txt");
const meta = await files.head("hello.txt");
const items = await files.list();
await files.delete("hello.txt");
import { Files } from "files-sdk";
import { r2 } from "files-sdk/r2";

const files = new Files({
  adapter: r2({ bucket: "uploads", accountId: "..." }),
});

await files.upload("hello.txt", "world");
const file = await files.download("hello.txt");
const meta = await files.head("hello.txt");
const items = await files.list();
await files.delete("hello.txt");
import { Files } from "files-sdk";
import { vercelBlob } from "files-sdk/vercel-blob";

const files = new Files({
  adapter: vercelBlob(),
});

await files.upload("hello.txt", "world");
const file = await files.download("hello.txt");
const meta = await files.head("hello.txt");
const items = await files.list();
await files.delete("hello.txt");
import { Files } from "files-sdk";
import { netlifyBlobs } from "files-sdk/netlify-blobs";

const files = new Files({
  adapter: netlifyBlobs({ name: "uploads" }),
});

await files.upload("hello.txt", "world");
const file = await files.download("hello.txt");
const meta = await files.head("hello.txt");
const items = await files.list();
await files.delete("hello.txt");
import { Files } from "files-sdk";
import { minio } from "files-sdk/minio";

const files = new Files({
  adapter: minio({ bucket: "uploads", endpoint: "http://localhost:9000" }),
});

await files.upload("hello.txt", "world");
const file = await files.download("hello.txt");
const meta = await files.head("hello.txt");
const items = await files.list();
await files.delete("hello.txt");

Capabilities

Everything you do with files.

A complete set of operations, 40+ adapters behind one interface, ready-made AI tools, and a CLI — plus multipart, live progress, byte ranges, and lifecycle hooks. The same one-line ergonomics on every backend.

Every operation, one interface

upload, download, head, exists, copy, move, list, delete — the same calls on every adapter. Hand any of them an array to batch with bounded concurrency, or walk a listing as a plain async iterable.

Read the docs
await files.upload("report.pdf", body);
const file = await files.download("report.pdf");

await files.copy("a.png", "b.png");
await files.move("tmp/x.png", "img/x.png");

// walk every page as a plain async iterable
for await (const f of files.listAll({ prefix: "img/" })) {
  console.log(f.key, f.size);
}

// pass an array to batch with bounded concurrency
await files.delete(["old/1.png", "old/2.png"]);
db.tar210 MB
tmp/hero.jpg4.2 MB
report.pdf184 KB
backup/report.pdf184 KB

Find files by name, glob, or regex

files.search() finds objects by key — a glob by default, or a regex, substring, or exact match. Matches stream back as an async iterable, and a glob's prefix scopes the walk to skip the rest of the bucket.

Read the docs
// glob by default — ** spans folders
for await (const file of files.search("invoices/**/*.pdf")) {
  console.log(file.key, file.size);
}

// or a regex, substring, or exact match
const errors = files.search(/error|panic/, { prefix: "logs/" });

// collect into an array, capped
const recent = await Array.fromAsync(
  files.search("*.png", { maxResults: 20 }),
);
0 matches
invoices/2024/q1.pdf184 KB
invoices/2024/q2.pdf176 KB
invoices/2023/q4.pdf201 KB
invoices/2023/q3.pdf192 KB

File tools for your agents

Generate ready-made file tools for the Vercel AI SDK, OpenAI Agents, or Claude and MCP. Hand your agent list, read, upload, and delete — with read-only mode and per-tool approval gates built in.

Read the docs
import { createFileTools } from "files-sdk/ai-sdk";
import { generateText } from "ai";

const tools = createFileTools({
  files,
  requireApproval: { deleteFile: true },
});

await generateText({
  model,
  tools, // listFiles, downloadFile, uploadFile, …
  prompt: "Archive last month's invoices to /archive.",
});

Archive last month's invoices to /archive.

listFiles("invoices/")
24 files
downloadFile("invoices/mar.pdf")
182 KB
uploadFile("archive/mar.pdf")
done
deleteFile("invoices/mar.pdf")
awaiting approval

The same SDK, from your shell

Every method is also a command. Stream with stdin and stdout, switch backends with --provider, and get JSON by default — handy for scripts, CI, and one-off ops.

Read the docs
# upload from a pipe, switch providers with a flag
cat q1.pdf | files --provider s3 upload q1.pdf --stdin

# list as JSON — the default
files --provider r2 list --prefix reports/

# stream a download straight to disk
files --provider gcs download q1.pdf --stdout > out.pdf
$
{"key":"reports/q1.pdf","size":184320}
{"key":"reports/q2.pdf","size":201618}
{"key":"reports/q3.pdf","size":176244}
$

Multipart, in parallel

Hand off a large body or an unbounded stream and files-sdk splits it into parts, uploading them with bounded concurrency. Tune the part size and parallelism, or just say multipart: true.

Read the docs
// split a large body into parallel parts
await files.upload("db.tar", stream, {
  multipart: true,
});

// or tune the part size & concurrency
await files.upload("db.tar", stream, {
  multipart: {
    partSize: 16 * 1024 * 1024,
    concurrency: 4,
  },
});
Part 1
Part 2
Part 3
Part 4
Part 5
Part 6
Part 7
Part 8
Part 9
Part 10
Part 11
Part 12
0 / 12 parts

Live upload progress

Pass one callback and get byte-level progress for every file — buffered or streamed, single or bulk. Drive a progress bar per key without ever touching the transport.

Read the docs
const items = [
  { key: "hero.jpg", body: hero },
  { key: "promo.mp4", body: promo },
  // …two more
];

await files.upload(items, {
  onProgress({ key, loaded, total }) {
    bars.get(key)?.set(loaded / total);
  },
});
logo.png240 KB0%
hero.jpg4.2 MB0%
promo.mp4128 MB0%
db.tar210 MB0%

Byte-range downloads

Ask for exactly the bytes you need. Ranged reads map straight to HTTP 206, so you can seek video, resume a download, or read a file header without pulling the whole object.

Read the docs
// download just a byte range — end is inclusive
const head = await files.download("video.mp4", {
  range: { start: 0, end: 1023 },
});

// stream the next chunk as the player seeks
const chunk = await files.download("video.mp4", {
  as: "stream",
  range: { start: offset, end: offset + CHUNK },
});
video.mp42:15
0:00 / 2:15
bytes=0-4194303206 Partial Content

Lifecycle hooks

Wire metrics, logging, and error reporting once at the constructor. onAction, onRetry, and onError fire for every operation across every adapter — fire-and-forget, never in your way.

Read the docs
const files = new Files({
  adapter: s3({ bucket: "uploads" }),
  hooks: {
    onAction({ type, status, durationMs }) {
      metrics.timing("files." + type, durationMs);
    },
    onRetry({ type, attempt }) {
      log.warn("retry " + attempt + ": " + type);
    },
    onError({ error }) {
      if (!error.aborted) Sentry.captureException(error);
    },
  },
});
files.uploadonAction142ms
files.downloadonAction88ms
files.uploadonRetryattempt 1
files.uploadonAction206ms
files.deleteonErrorNotFound
files.listonAction51ms

Mirror across backends

sync() reconciles one backend onto another — uploading only what changed, skipping what's identical, and pruning what's gone. Back up or migrate in a line, and dry-run the plan first.

Read the docs
const live = new Files({ adapter: s3({ bucket: "live" }) });
const backup = new Files({
  adapter: r2({
    bucket: "backup",
    accountId,
    accessKeyId,
    secretAccessKey,
  }),
});

// back up S3 to R2 — only the delta moves
const {
  uploaded,
  skipped,
  deleted,
} = await sync(live, backup, {
  prune: true,
  compare: "size",
});
SyncingS3S3toR2R2mirroring…
reports/q1.pdfcomparing…
img/logo.pngcomparing…
img/hero.jpgcomparing…
data/2026.csvcomparing…
tmp/legacy.zipcomparing…

Two steps

Your first upload in under a minute.

Install files-sdk and your provider's native client, then construct one Files instance and start calling it.

1. Install

npm install files-sdk
pnpm add files-sdk
bun add files-sdk
yarn add files-sdk

Each adapter's native SDK is an optional peer dependency — install only the ones you actually use.

2. Make your first call

import { Files } from "files-sdk";
import { s3 } from "files-sdk/s3";

const files = new Files({
  adapter: s3({ bucket: "uploads", region: "us-east-1" }),
});

await files.upload("hello.txt", "world");

Construct a Files instance with your provider's adapter, then call upload, download, list, delete on it.

Ship the storage layer once.

Open source, MIT licensed, built around web standards. Drop in an adapter and forget the difference.

Read the docs