Any Storage Driver

Put a unfs filesystem wherever unstorage reaches.

unfs asks very little of its backend: a string-keyed, byte-valued namespace with get and set. unstorage is exactly that, behind around 40 drivers. unfs/unstorage maps one onto the other.

The local disk, Redis, S3, Cloudflare KV and R2, Vercel Blob, Netlify Blobs, a SQL table via db0, IndexedDB or localStorage in the browser, HTTP, an in-memory Map. Whatever unstorage can talk to can hold the Merkle DAG.

npm install unfs unstorage

unstorage is an optional peer dependency, used for its types and never for a value.

#A store on any driver

import { createFs, initStore } from "unfs";
import { unstorageStore } from "unfs/unstorage";
import { createStorage } from "unstorage";
import fsLiteDriver from "unstorage/drivers/fs-lite";

const storage = createStorage({ driver: fsLiteDriver({ base: ".data/store" }) });
const fs = createFs(await initStore(unstorageStore(storage)));

await fs.mkdir("/notes", { recursive: true });
await fs.writeFile("/notes/hello.txt", "hello from unstorage\n");

await fs.head(); // bafyrei… — the root CID of what's now on disk

That's the whole integration. unstorageStore implements every optional KVStore capability, so packs, existence checks, and gc all work:

KVStoreunstorage
get(key)getItemRaw(key), nullundefined
set(key, value)setItemRaw(key, value)
has(key)hasItem(key)
delete(key)removeItem(key)
keys(prefix)getKeys(prefix)

#Reopening an existing store

A new Storage over the same driver is all it takes. A store that already carries its writer parameters is opened rather than initialized, so reach for openStore, and it resolves to the same root CID a previous process left behind. Nothing is held in memory.

const existing = await storage.hasItem("meta:format");
const store = existing
  ? await openStore(unstorageStore(storage))
  : await initStore(unstorageStore(storage));

#Splitting the key spaces

unfs's two key spaces have opposite characters. blocks: is immutable and content-addressed, so it's safe to cache forever, replicate, or serve from a CDN. refs: is a handful of tiny mutable pointers that want a backend with atomic writes.

unstorage mounts put each where it belongs behind one Storage, and unfs never sees the seam:

const storage = createStorage({ driver: memoryDriver() });
storage.mount("blocks", s3Driver({ ... })); // immutable, cacheable, big
storage.mount("refs", redisDriver({ ... })); // tiny, mutable, atomic

Sharing one Storage with data that isn't unfs's works the other way round: put unfs under a prefix, using prefixedStore from unfs or unstorage's own prefixStorage.

#Three things to know

  • Raw, always. getItem and setItem run values through JSON, which would mangle a DAG-CBOR block, so the adapter only ever uses getItemRaw and setItemRaw. Drivers that hold bytes serve those natively, and unstorage base64-round-trips them for the ones that don't. A driver reply that's neither Uint8Array nor ArrayBuffer is refused rather than coerced, since the bytes are already gone and guessing an encoding would corrupt the DAG.
  • The store is async, even over the memory driver, so createFsSync is out. The synchronous facade needs a driver that returns plain values, which is what memoryStore() is for.
  • Durability and atomicity are the driver's. unfs writes a block before the ref that names it, so an interrupted write leaves unreachable blocks (which gc sweeps) rather than a dangling root. Atomicity of the refs: update itself is whatever the driver gives you, so with concurrent writers, mount refs: somewhere that has it. This adapter also exposes neither setIfMatch nor deleteIfMatch, so compare-and-set and compare-and-delete aren't available through it — unstorage's own conditional writes are newer than the version range here, and a capability that's present but unsupported is worse than one that's missing. Anything that requires one says so (UNSUPPORTED) rather than racing: a git host over this adapter refuses ref deletion outright.

Keys need no escaping layer, incidentally. unstorage normalizes /, \, repeated :, and a leading or trailing :, and the spec bans exactly those in a ref name, while blocks: keys are base32. With a filesystem driver, the three key spaces are simply three folders on disk.

#A runnable example

examples/unstorage/ runs the demo above against fs-lite and keeps its store in .data/. Run it twice and the second run reopens the first run's tree at the same root CID. It also shows the key spaces as they land in the driver, split mounts, and a gc sweep over the driver's own key enumeration.

unfs  A filesystem you can put in any key–value store.