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 unstorageunstorage 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 diskThat's the whole integration. unstorageStore implements every optional KVStore capability, so packs, existence checks, and gc all work:
KVStore | unstorage |
|---|---|
get(key) | getItemRaw(key), null → undefined |
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, atomicSharing 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.
getItemandsetItemrun values through JSON, which would mangle a DAG-CBOR block, so the adapter only ever usesgetItemRawandsetItemRaw. Drivers that hold bytes serve those natively, and unstorage base64-round-trips them for the ones that don't. A driver reply that's neitherUint8ArraynorArrayBufferis 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
createFsSyncis out. The synchronous facade needs a driver that returns plain values, which is whatmemoryStore()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
gcsweeps) rather than a dangling root. Atomicity of therefs:update itself is whatever the driver gives you, so with concurrent writers, mountrefs:somewhere that has it. This adapter also exposes neithersetIfMatchnordeleteIfMatch, 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.