Introduction

Anywhere you can store key–value pairs, you can have a filesystem.

Files, directories, symlinks, permissions, and the node:fs API you already know, with no disk involved. Your data lives in whatever store you point it at: an in-memory map, a folder on disk, Redis, S3, a database. If it can get and put bytes, it can hold a filesystem.

import { createFs, initStore, memoryStore } from "unfs";

const fs = createFs(await initStore(memoryStore()));
await fs.writeFile("/hello.txt", "it's a filesystem!\n");

That's the whole setup. The Quick Start takes it from here.

#Why you'd want one

You need a filesystem where there isn't one. Serverless functions, browsers, tests that shouldn't touch the real disk, sandboxes for untrusted code or AI agents. It's plain TypeScript with no dependencies, so it runs wherever your code runs and keeps its data wherever you say.

You get history without asking for it. Every write produces a snapshot. Not as a feature you turn on, but simply as how the data is stored. The tree you had a moment ago is still there, and you can keep it, diff against it, or garbage-collect it later.

You can move whole trees around. Export any directory as a single hash-verified file and import it somewhere else. Or sync two stores directly, sending only the blocks the other side is missing.

#How it works

If you know roughly how git works, you already know this. Your files are stored as a graph of blocks with three properties:

  • Every block is named by the hash of its content. Write the same bytes twice and they're stored once. Two directories with the same subtree share it.
  • Blocks are never modified. A change writes new blocks, and the old ones stay where they were.
  • One named ref points at the current root. Every change builds a new tree and moves that ref one step forward. It's called "head" unless you say otherwise.

Everything unfs is good at follows from those three facts. Copying a 10,000-file directory is instant, because a copy is a second pointer to the same blocks. Snapshots cost nothing, because a snapshot is just a root that stopped moving. Packs and sync stay simple, because a tree is fully described by a set of hash-named blocks that any store can check for itself.

Two more properties are worth knowing about, even if you never touch them directly:

  • Writes are deterministic. The same tree always produces the same blocks and the same ref, whatever order you wrote it in, on whatever machine, at whatever time. Timestamps only appear if you opt in.
  • The format is open. Blocks are DAG-CBOR, refs are CIDs, and packs are CARv1 files. Standard IPLD tooling can read and move unfs data, and the whole wire format is written down in a self-contained spec.

#What it isn't

Three boundaries worth knowing before you start:

  • Not a mounted filesystem. It's a library. Your code talks to it through its API, and other programs on the machine don't see a drive.
  • Not access control. Modes and owners are recorded faithfully, but nothing enforces them.
  • Not stable yet. This is pre-1.0. The API works and is tested, but expect breaking changes between releases.

#Next steps

  • Quick Start — install it and build your first tree.
  • The Filesystem API — the full surface, watching for changes, and the synchronous version.
  • Performance — what to reach for when writes or reads get slow.
  • Packs & Sync — moving trees between stores, and cleaning up afterwards.
  • Layered Stores — putting a fast store in front of a slow one.
  • IPFS — sharing a blockstore with kubo, and CAR files in both directions.
  • Under the Hood — working with the block graph directly.
  • Integrations — picking a storage driver, or putting a bash sandbox on top.

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