Adapters

Overview

Every adapter implements the same Adapter contract: upload, download, head, list, copy, move, delete, url, uploadUrl, plus the snapshots and forks namespaces. Pick one based on your provider; the call site doesn’t change.

Snapshots and forks are native on Tigris (and its branded aliases Fly.io and Railway) and on GitHub — the adapter calls the provider’s native APIs and no data is copied. On GitHub specifically, every snapshot is a git tag and every fork is a git branch. On every other provider they’re emulated as sibling buckets, and the adapter does the bookkeeping via a small .storagesdk.metadata.json manifest.

Each adapter’s provider SDK is an optional peer dependency. Install only the SDKs for the adapters you actually import.

Runtime adapter selection

@storagesdk/adapters’s root export ships a small registry for runtime-driven adapter selection. CLIs, scripts, and any code that picks a backend from a string at runtime use it; library code that knows its one adapter at build time keeps using the subpath import.

import {
  ADAPTERS,
  type AdapterName,
  type AdapterEnvVar,
  buildAdapter,
  getAdapterEnvVars,
} from '@storagesdk/adapters';

Quick use

import { Storage } from '@storagesdk/core';
import { buildAdapter } from '@storagesdk/adapters';

const storage = new Storage({ adapter: await buildAdapter('tigris') });
// reads TIGRIS_BUCKET, TIGRIS_ACCESS_KEY_ID, TIGRIS_SECRET_ACCESS_KEY
// from process.env and dynamically imports just the tigris adapter

buildAdapter(name) does three things: dynamic-import the factory, read env vars into a config, call the factory. One async call at startup, fully-constructed adapter back.

Introspecting env vars

getAdapterEnvVars('tigris')
// → readonly [
//   { name: 'TIGRIS_BUCKET', required: true },
//   { name: 'TIGRIS_ACCESS_KEY_ID', required: true },
//   { name: 'TIGRIS_SECRET_ACCESS_KEY', required: true },
//   { name: 'TIGRIS_ENDPOINT', required: false },
//   { name: 'TIGRIS_FORCE_PATH_STYLE', required: false },
// ]

interface AdapterEnvVar {
  readonly name: string;
  readonly required: boolean;
  readonly fallback?: readonly string[];
}

fallback lists backend-native env vars that get consulted when the adapter-prefixed one isn’t set — S3_ACCESS_KEY_ID falls back to AWS_ACCESS_KEY_ID, GCS_PROJECT_ID to GOOGLE_CLOUD_PROJECT, VERCEL_BLOB_TOKEN to BLOB_READ_WRITE_TOKEN, and so on.

Iterate ADAPTERS to enumerate every adapter and its spec — useful for CLI help text, validation, or generating docs:

import { ADAPTERS, getAdapterEnvVars } from '@storagesdk/adapters';

for (const name of ADAPTERS) {
  console.log(name, getAdapterEnvVars(name));
}

Each adapter’s dependencies load on demand

buildAdapter('tigris') is the only call that pulls @tigrisdata/storage into your runtime. The other 16 adapters’ peer-SDK packages — @aws-sdk/client-s3, @azure/storage-blob, @google-cloud/storage, @octokit/rest, and so on — are never imported.

That’s why buildAdapter is async: it import()s the requested adapter’s implementation lazily. Modern bundlers code-split each dynamic import into its own chunk; runtimes execute only the one that gets called.

ADAPTERS and getAdapterEnvVars are synchronous — they’re just static data.

One adapter, tree-shakeable

When you know your adapter at build time, import it directly:

import { tigris } from '@storagesdk/adapters/tigris';
const storage = new Storage({ adapter: tigris({ ... }) });

Only the one adapter and its peer SDK end up in your bundle — the registry adds nothing, the other 16 adapters tree-shake out.

Discovery example

Run the bundled discovery script to see every adapter with its env vars:

pnpm --filter @storagesdk/examples adapters

Source: examples/adapters/index.ts.