Skip to content

client.vfs

client.storage gives you files identified by a manifest. client.vfs gives you a filesystem: directories, paths you can navigate, version history, and copies that cost nothing.

The bytes go through client.storage.writeFile exactly as they always have, into a real Space with a real gated manifest. What the VFS adds is the metadata: one encrypted record per directory in your personal space, each listing only its immediate children.

The VFS has a working directory, so paths behave the way they do in a shell.

await client.vfs.cd("/apps/scratch");
client.vfs.cwd; // "/apps/scratch"
await client.vfs.readText("src/App.tsx"); // relative to cwd
await client.vfs.readText("/other.txt"); // absolute, unaffected
await client.vfs.cd(".."); // climbs; stops at the root

cd checks the target is a real directory, so a typo fails there — where the error can name the path — instead of turning every later relative path into a puzzling “does not exist”.

It lives here rather than in each consumer so a CLI, a script and an app all agree on what “here” means. muhkoo vfs cd is this, persisted between commands.

await client.vfs.list("/apps/my-app"); // immediate children, dirs first
await client.vfs.stat("/apps/my-app/App.tsx");
await client.vfs.exists("/apps/my-app");
await client.vfs.walk("/apps"); // every file path beneath, depth-first
await client.vfs.readText("/apps/my-app/App.tsx");
await client.vfs.readFile("/apps/my-app/logo.png"); // Uint8Array
await client.vfs.glob("/apps/**/*.{ts,tsx}");

list is one record fetch. walk and glob are one fetch per directory in the subtree — cheap for a project, worth thinking about at the root of a large filesystem.

await client.vfs.writeFile("/apps/my-app/src/App.tsx", source); // parents created
await client.vfs.writeFile("/apps/my-app/logo.png", pngBytes); // binary is fine
await client.vfs.mkdir("/apps/my-app/assets");
await client.vfs.rename("/apps/my-app", "/archive/my-app"); // also moves
await client.vfs.copy("/apps/my-app", "/apps/my-app-2");
await client.vfs.delete("/archive", { recursive: true });

Every mutation is a read-modify-write of exactly one directory record, so a write cannot leave the tree half-updated: either the parent names the new child or it does not.

Moving a directory is O(1) however much is inside it. Nothing in a subtree records its own path, so re-parenting edits one entry and the subtree is not touched.

Copying a file moves no bytes. Content is immutable and content-addressed, so the copy is a second handle to the same shards — duplicating a 200 MB asset is one small metadata write. The copy does get a fresh identity, so the two files do not share version history.

Every write keeps the previous manifest, so you get undo that survives a reload.

await client.vfs.history("/apps/my-app/App.tsx"); // newest first
await client.vfs.restore("/apps/my-app/App.tsx", 0);

Restoring pushes the current version onto history first, so it is itself undoable. Unchanged chunks dedupe in the shard store, so keeping versions costs little beyond the history record — which is capped per file (default 20).

The root key is derived from your master seed, which lives in memory only and is lost on reload until you sign in again. While it is missing, every VFS call throws VfsLockedError.

if (!client.vfs.unlocked) await client.auth.zk.resume();
const stop = client.vfs.watch(() => {
// re-read whatever you are showing
});

Fires when your filesystem is written somewhere else — another tab, another machine, the CLI. It shares the personal space’s websocket with client.kv, so subscribing costs no extra connection.

It is a notification, not a diff: the change names a record, and a record is not a path, so the cache is dropped and you re-read what you are actually showing. In a runtime with no WebSocket it is a no-op — watching is an optimisation, never required for correctness.

const { removed } = await client.vfs.sweep();

Because each write touches a single record, two tabs editing the same directory can leave a child whose parent entry lost the race. Such a record is unreachable rather than corrupting, but it costs storage. sweep() reclaims them. Run it when the filesystem is idle — a sweep racing an in-flight write could remove a record that is about to be referenced.

Only the root key comes from your master seed. Every other directory has a random key stored in its parent’s entry, which chains capabilities downward: holding {id, key} for a subtree lets you walk that subtree and nothing above it, because no key can be derived upward.

That is the seam sharing hangs off — a subtree can later be handed to a Space without re-encrypting the rest of the filesystem.