Introduction
mountx takes a filesystem you wrote in JavaScript and makes it a real folder on the machine.
You write an object with the same methods as node:fs/promises — stat, readdir, open, and whichever of mkdir, rename, unlink make sense for you. mountx serves it to the kernel, and from then on ls, cat, git, VS Code, a build tool or an AI agent can use it without knowing there is any JavaScript involved.
import { mount } from "mountx/auto";
import { createMemoryDriver } from "mountx/drivers/memory";
await using mounted = await mount(createMemoryDriver(), "/mnt/point");
// /mnt/point is now a real folderWarning
Alpha. The API can still change, and mountx has not been through a security or correctness audit. Once mounted, a driver is reachable by every program on the machine.
#Why you'd want one
Because a path is the most portable interface there is. Anything that reads files can read yours: no plugin, no SDK, no virtual-filesystem shim per tool. An in-memory store, a zip file, an S3 bucket, a database table, a remote API, or a plain folder with your own rules layered on top — give it a path and every program on the machine already knows how to use it.
Because there is no new API to learn. FsDriver is deliberately a subset of node:fs/promises, to the point where node:fs/promises itself is a valid driver with no adaptation. So is anything already shaped like it. The errors are node:fs errors too — throw a code: "ENOENT" and the kernel gets ENOENT.
Because you can build it without mounting anything. createLoopback(driver) runs your driver in-process with the same path normalization and capability resolution a real mount applies. No kernel, no privileges, works on Windows. Then you swap one line and it is a folder.
#The mental model
Three layers, and only the middle one is yours:
A program calls open("/mnt/point/notes.txt"). The kernel turns that into protocol messages, mountx decodes them and calls the methods on your driver, and the values you return are encoded back. Your driver never sees a protocol message, a file descriptor number, or a kernel flag it did not ask for — it sees absolute POSIX paths and returns fs-shaped values.
Two consequences worth holding on to:
- Serving never needs privileges. Both protocol implementations are pure JavaScript. Only attaching the result to a directory involves the operating system — see Do I need root?
- Nothing is faked. If your driver has no
symlink, symlinks are not supported, and the mount saysENOSYSrather than inventing behaviour. That's the capability rule, and it holds all the way down.
#The two transports
| FUSE | NFSv3 | |
|---|---|---|
| mounts on | Linux | Linux, macOS |
| root to mount | no, with fusermount3 | Linux: yes · macOS: no |
| serves to | the local kernel | anything with an NFSv3 client |
| what you lose | nothing | handles — a deleted-but-open file goes ESTALE |
mountx/auto picks between them from host facts, so the same code mounts on Linux and macOS. You can also pin one yourself.
#What mountx is not
- Not a filesystem. It's the plumbing that carries one. The filesystem is the driver you write (or one of the two bundled ones).
- Not a way to use your own mount from the process serving it. That deadlocks — see the threadpool hazard.
- Not able to create special files. No FIFOs, sockets or device nodes, because
node:fs/promisescannot create them either. - Not stable yet. Pre-1.0, pre-audit.
#Next steps
- Quick Start — install it and have a folder in about a minute.
npx mountx— mount a demo filesystem right now and watch the kernel talk to it.- Drivers — the three built-in ones, and the interface for writing your own.
- Mounting —
mount(), the mount object, lifecycle and unmount. - Tuning — caching, concurrency, and the options worth knowing.
- Troubleshooting — the things that will bite you, and how to recover.
- Transports — what
autois choosing between, and how to pin one. - API reference — every entry point, option and type.