Storage
@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.
| Export | Where bytes live | Needs an account | Surface |
|---|---|---|---|
fs | OPFS first, best-effort cloud replication | optional | common subset (sync, callbacks, promises) |
opfs | the browser’s Origin Private File System | no | common subset (sync, callbacks, promises) |
cloud.fs | FKN cloud storage | yes | async only (promises + callbacks) |
Each is also a Node-style subpath import: @fkn/lib/fs, @fkn/lib/opfs, @fkn/lib/cloud/fs (and their /promises forms).
The default: fs
Section titled “The default: fs”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.
// Hydrate the in-memory layer from storage once, before any synchronous read.await (alias) namespace fsimport fs
fs.index_d_exports.mount(): Promise<void>export index_d_exports.mount
mount()
(alias) namespace fsimport fs
fs.index_d_exports.writeFileSync(path: import("node:fs").PathLike, data: WriteData$1, options?: WriteOptions): voidexport index_d_exports.writeFileSync
writeFileSync('/library/state.json', var JSON: JSON
An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.
JSON.JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)
Converts a JavaScript value to a JavaScript Object Notation (JSON) string.
stringify({ items: never[]
items: [] }))const const state: string | Buffer<ArrayBufferLike>
state = (alias) namespace fsimport fs
fs.index_d_exports.readFileSync(path: import("node:fs").PathLike, options?: ReadOptions): Buffer | stringexport index_d_exports.readFileSync
readFileSync('/library/state.json', 'utf8')
// fs/promises, same files:await (alias) namespace fsimport fs
fs.const index_d_exports.promises: { readFile: (path: import("node:fs").PathLike, options?: ReadOptions) => Promise<Buffer | string>; writeFile: (path: import("node:fs").PathLike, data: WriteData$1, options?: WriteOptions) => Promise<void>; appendFile: (path: import("node:fs").PathLike, data: WriteData$1, options?: WriteOptions) => Promise<void>; stat: (path: import("node:fs").PathLike) => Promise<fs.Stats>; lstat: (path: import("node:fs").PathLike) => Promise<fs.Stats>; ... 6 more ...; access: (path: import("node:fs").PathLike) => Promise<void>;}export index_d_exports.promises
promises.writeFile: (path: import("node:fs").PathLike, data: WriteData$1, options?: WriteOptions) => Promise<void>
writeFile('/library/notes.txt', 'hello')
// Force-refresh a file from the cloud to pick up another device's change.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-only: cloud.fs
Section titled “Cloud-only: cloud.fs”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().
await (alias) namespace cloudimport cloud
cloud.namespace cloud_d_exports.fsexport cloud_d_exports.fs
fs.const fs_d_exports.promises: { readFile: (path: import("node:fs").PathLike, options?: ReadOptions) => Promise<Buffer | string>; writeFile: (path: import("node:fs").PathLike, data: WriteData$1, options?: WriteOptions) => Promise<void>; unlink: (path: import("node:fs").PathLike) => Promise<void>; rename: (from: import("node:fs").PathLike, to: import("node:fs").PathLike) => Promise<void>; readdir: (path: import("node:fs").PathLike) => Promise<string[]>; mkdir: (_path?: import("node:fs").PathLike, _options?: MakeOptions) => Promise<void>; ... 4 more ...; access: (path: import("node:fs").PathLike) => Promise<void>;}export fs_d_exports.promises
promises.writeFile: (path: import("node:fs").PathLike, data: WriteData$1, options?: WriteOptions) => Promise<void>
writeFile('save/profile.json', var JSON: JSON
An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.
JSON.JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)
Converts a JavaScript value to a JavaScript Object Notation (JSON) string.
stringify({ level: number
level: 7 })) const const profile: string | Buffer<ArrayBufferLike>
profile = await (alias) namespace cloudimport cloud
cloud.namespace cloud_d_exports.fsexport cloud_d_exports.fs
fs.const fs_d_exports.promises: { readFile: (path: import("node:fs").PathLike, options?: ReadOptions) => Promise<Buffer | string>; writeFile: (path: import("node:fs").PathLike, data: WriteData$1, options?: WriteOptions) => Promise<void>; unlink: (path: import("node:fs").PathLike) => Promise<void>; rename: (from: import("node:fs").PathLike, to: import("node:fs").PathLike) => Promise<void>; readdir: (path: import("node:fs").PathLike) => Promise<string[]>; mkdir: (_path?: import("node:fs").PathLike, _options?: MakeOptions) => Promise<void>; ... 4 more ...; access: (path: import("node:fs").PathLike) => Promise<void>;}export fs_d_exports.promises
promises.readFile: (path: import("node:fs").PathLike, options?: ReadOptions) => Promise<Buffer | string>
readFile('save/profile.json', 'utf8')
const const files: string[]
files = await (alias) namespace cloudimport cloud
cloud.namespace cloud_d_exports.fsexport cloud_d_exports.fs
fs.const fs_d_exports.promises: { readFile: (path: import("node:fs").PathLike, options?: ReadOptions) => Promise<Buffer | string>; writeFile: (path: import("node:fs").PathLike, data: WriteData$1, options?: WriteOptions) => Promise<void>; unlink: (path: import("node:fs").PathLike) => Promise<void>; rename: (from: import("node:fs").PathLike, to: import("node:fs").PathLike) => Promise<void>; readdir: (path: import("node:fs").PathLike) => Promise<string[]>; mkdir: (_path?: import("node:fs").PathLike, _options?: MakeOptions) => Promise<void>; ... 4 more ...; access: (path: import("node:fs").PathLike) => Promise<void>;}export fs_d_exports.promises
promises.readdir: (path: import("node:fs").PathLike) => Promise<string[]>
readdir('save') const { const usedBytes: number
usedBytes, const limitBytes: number
limitBytes, const objects: number
objects, const maxObjects: number
maxObjects } = await (alias) namespace cloudimport cloud
cloud.namespace cloud_d_exports.fsexport cloud_d_exports.fs
fs.fs_d_exports.quota(): Promise<cloud.fs.StorageQuota>export fs_d_exports.quota
quota()}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.
Local-only: opfs
Section titled “Local-only: opfs”opfs exposes the same supported Node-compatible subset entirely on the device, with no account or network.
await (alias) namespace opfsimport opfs
opfs.index_d_exports.mount(): Promise<void>export index_d_exports.mount
mount()(alias) namespace opfsimport opfs
opfs.index_d_exports.writeFileSync(path: import("node:fs").PathLike, data: WriteData$1, options?: WriteOptions): voidexport index_d_exports.writeFileSync
writeFileSync('/cache/poster.bin', new var Uint8Array: Uint8ArrayConstructornew (elements: Iterable<number>) => Uint8Array<ArrayBuffer> (+6 overloads)
Uint8Array([255, 216, 255]))Content type
Section titled “Content 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:
const const pngBytes: Uint8Array<ArrayBuffer>
pngBytes = new var Uint8Array: Uint8ArrayConstructornew (elements: Iterable<number>) => Uint8Array<ArrayBuffer> (+6 overloads)
Uint8Array([137, 80, 78, 71])await (alias) namespace cloudimport cloud
cloud.namespace cloud_d_exports.fsexport cloud_d_exports.fs
fs.const fs_d_exports.promises: { readFile: (path: import("node:fs").PathLike, options?: ReadOptions) => Promise<Buffer | string>; writeFile: (path: import("node:fs").PathLike, data: WriteData$1, options?: WriteOptions) => Promise<void>; unlink: (path: import("node:fs").PathLike) => Promise<void>; rename: (from: import("node:fs").PathLike, to: import("node:fs").PathLike) => Promise<void>; readdir: (path: import("node:fs").PathLike) => Promise<string[]>; mkdir: (_path?: import("node:fs").PathLike, _options?: MakeOptions) => Promise<void>; ... 4 more ...; access: (path: import("node:fs").PathLike) => Promise<void>;}export fs_d_exports.promises
promises.writeFile: (path: import("node:fs").PathLike, data: WriteData$1, options?: WriteOptions) => Promise<void>
writeFile('save/cover', const pngBytes: Uint8Array<ArrayBuffer>
pngBytes, { contentType?: string | undefined
contentType: 'image/png' })Encrypted contents
Section titled “Encrypted contents”Cloud file contents are always end-to-end encrypted. There is no unencrypted mode and apps do not control it: 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 the sealing 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 one thing to handle is the locked state: until the user unlocks in this session, reads and writes reject with a StorageLockedError (its code is FKN_E2E_LOCKED). Prompt for an unlock and retry:
try { await (alias) namespace cloudimport cloud
cloud.namespace cloud_d_exports.fsexport cloud_d_exports.fs
fs.const fs_d_exports.promises: { readFile: (path: import("node:fs").PathLike, options?: ReadOptions) => Promise<Buffer | string>; writeFile: (path: import("node:fs").PathLike, data: WriteData$1, options?: WriteOptions) => Promise<void>; unlink: (path: import("node:fs").PathLike) => Promise<void>; rename: (from: import("node:fs").PathLike, to: import("node:fs").PathLike) => Promise<void>; readdir: (path: import("node:fs").PathLike) => Promise<string[]>; mkdir: (_path?: import("node:fs").PathLike, _options?: MakeOptions) => Promise<void>; ... 4 more ...; access: (path: import("node:fs").PathLike) => Promise<void>;}export fs_d_exports.promises
promises.readFile: (path: import("node:fs").PathLike, options?: ReadOptions) => Promise<Buffer | string>
readFile('save/profile.json')} catch (var error: unknown
error) { if (var error: unknown
error instanceof (alias) namespace cloudimport cloud
cloud.namespace cloud_d_exports.fsexport cloud_d_exports.fs
fs.class fs_d_exports.StorageLockedErrorexport fs_d_exports.StorageLockedError
StorageLockedError) { // shows the first-party unlock card; resolves true once unlocked if (await (alias) namespace cloudimport cloud
cloud.namespace cloud_d_exports.fsexport cloud_d_exports.fs
fs.fs_d_exports.unlock(): Promise<boolean>export fs_d_exports.unlock
unlock()) await (alias) namespace cloudimport cloud
cloud.namespace cloud_d_exports.fsexport cloud_d_exports.fs
fs.const fs_d_exports.promises: { readFile: (path: import("node:fs").PathLike, options?: ReadOptions) => Promise<Buffer | string>; writeFile: (path: import("node:fs").PathLike, data: WriteData$1, options?: WriteOptions) => Promise<void>; unlink: (path: import("node:fs").PathLike) => Promise<void>; rename: (from: import("node:fs").PathLike, to: import("node:fs").PathLike) => Promise<void>; readdir: (path: import("node:fs").PathLike) => Promise<string[]>; mkdir: (_path?: import("node:fs").PathLike, _options?: MakeOptions) => Promise<void>; ... 4 more ...; access: (path: import("node:fs").PathLike) => Promise<void>;}export fs_d_exports.promises
promises.readFile: (path: import("node:fs").PathLike, options?: ReadOptions) => Promise<Buffer | string>
readFile('save/profile.json') } else { throw var error: unknown
error }}cloud.fs.encryption() reports the current state for this app without triggering a prompt, so you can reflect it in your UI:
const { const unlocked: boolean
unlocked, const enrolled: boolean
enrolled, const keyEpoch: number | null
keyEpoch } = await (alias) namespace cloudimport cloud
cloud.namespace cloud_d_exports.fsexport cloud_d_exports.fs
fs.fs_d_exports.encryption(): Promise<cloud.fs.EncryptionStatus>export fs_d_exports.encryption
encryption()// unlocked is false until the user unlocks in this session; enrolled is false when the// account has not finished creating its keys yet, which is when nothing can be storedStorageLockedError deliberately does not read as a missing file, so a sync layer that treats read failures as “empty” must handle locked as unavailable instead.
Storage scope
Section titled “Storage scope”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.