@fkn/lib gives browser apps a common path-based subset of Node.js fs, including readFile, writeFile, callback forms, synchronous forms, and promises. It comes in three variants with different storage and account scopes.
The bare fs export writes to OPFS first and starts cloud replication in the background when an account is connected. It provides synchronous calls, callbacks, and fs/promises for the supported operation set.
There is no synchronous disk in a browser, so fs keeps an in-memory layer that synchronous calls read and
write instantly; mount() hydrates it from storage, and a synchronous read issued before it resolves sees
ENOENT. The promise and callback forms await the mount internally. Writes flush to OPFS on a debounce and on tab-hide. fs.flush() attempts all pending hybrid backing writes, but it catches backing failures and still resolves; it is not a durability acknowledgment. A best-effort cloud copy can also finish later. Use cloud.fs when the call must await a cloud write.
cloud.fs writes straight through to the user’s cloud storage and is durable the moment the promise resolves.
Because there is no synchronous network, it is async only - promises and callbacks, no *Sync. It needs
a connected account, so guard on cloud.fs.available().
cloud.fs.readFileSync does not exist - it is a compile error, not a runtime surprise. The same goes for file
descriptors, streams, and positional writes: the cloud backing cannot do them, so they are absent from its type.
Node’s fs has no notion of a MIME type, but cloud blobs need one. On a cloud write it is inferred from the
path extension (.json becomes application/json), falling back to application/octet-stream. Override it
with the contentType option:
Cloud file contents can be end-to-end encrypted. It is opt-in and the user turns it on per app from their account settings: apps do not control it and cannot read the key. When a scope is set to encrypted, the browser seals each file’s contents on the device before upload, and the service stores only sealed bytes it cannot open. Names, sizes, paths, and content types stay visible so the service can store the data and count it toward the quota. This is transparent to cloud.fs: the same readFile and writeFile calls work, and encryption happens in the FKN broker, so even an app pinned to an older @fkn/lib gets it.
Your app never handles keys or ciphertext. The only new thing to handle is the locked state: when a scope is encrypted and the user has not unlocked it in this session, writes and reads of encrypted files reject with a StorageLockedError (its code is FKN_E2E_LOCKED). Files saved in standard format before encryption was turned on still read while locked. Prompt for an unlock and retry:
// appMode is 'encrypted' or 'plaintext'; unlocked is false until the user unlocks this session
StorageLockedError deliberately does not read as a missing file, so a sync layer that treats read failures as “empty” must handle locked as unavailable instead.
OPFS data belongs to the embedding page’s browser origin and local browser profile. It is not account-scoped. Cloud files belong to the connected account and the embedding app’s origin, so two apps writing save/profile.json receive separate cloud objects. The cloud quota is account-wide across the user’s apps.
Paths are logical and cloud directories are implicit. The Node-compatible APIs normalize directory-oriented operations using Node-style path rules. Whole-file reads, writes, and removals are validated by the cloud storage service when they reach it.
For the bundler drop-in - aliasing bare fs / fs/promises so unmodified Node code resolves here - see
the Node fs polyfill.