Skip to content

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.

ExportWhere bytes liveNeeds an accountSurface
fsOPFS first, best-effort cloud replicationoptionalcommon subset (sync, callbacks, promises)
opfsthe browser’s Origin Private File Systemnocommon subset (sync, callbacks, promises)
cloud.fsFKN cloud storageyesasync 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 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.

import {
(alias) namespace fs
import fs
fs
} from '@fkn/lib'
// Hydrate the in-memory layer from storage once, before any synchronous read.
await
(alias) namespace fs
import fs
fs
.
index_d_exports.mount(): Promise<void>
export index_d_exports.mount
mount
()
(alias) namespace fs
import fs
fs
.
index_d_exports.writeFileSync(path: import("node:fs").PathLike, data: WriteData, options?: WriteOptions): void
export 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.

@paramvalue A JavaScript value, usually an object or array, to be converted.

@paramreplacer A function that transforms the results.

@paramspace Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.

@throws{TypeError} If a circular reference or a BigInt value is found.

stringify
({
items: never[]
items
: [] }))
const
const state: string | Buffer<ArrayBufferLike>
state
=
(alias) namespace fs
import fs
fs
.
index_d_exports.readFileSync(path: import("node:fs").PathLike, options?: ReadOptions): Buffer | string
export index_d_exports.readFileSync
readFileSync
('/library/state.json', 'utf8')
// fs/promises, same files:
await
(alias) namespace fs
import 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, options?: WriteOptions) => Promise<void>;
appendFile: (path: import("node:fs").PathLike, data: WriteData, 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, options?: WriteOptions) => Promise<void>
writeFile
('/library/notes.txt', 'hello')
// Force-refresh a file from the cloud to pick up another device's change.
await
(alias) namespace fs
import fs
fs
.
index_d_exports.pull(path: string): Promise<Blob>
export index_d_exports.pull
pull
('/library/state.json')

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().

import {
(alias) namespace cloud
import cloud
cloud
} from '@fkn/lib'
if (await
(alias) namespace cloud
import cloud
cloud
.
namespace cloud_d_exports.fs
export cloud_d_exports.fs
fs
.
fs_d_exports.available(): Promise<boolean>
export fs_d_exports.available
available
()) {
await
(alias) namespace cloud
import cloud
cloud
.
namespace cloud_d_exports.fs
export 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, 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, 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.

@paramvalue A JavaScript value, usually an object or array, to be converted.

@paramreplacer A function that transforms the results.

@paramspace Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.

@throws{TypeError} If a circular reference or a BigInt value is found.

stringify
({
level: number
level
: 7 }))
const
const profile: string | Buffer<ArrayBufferLike>
profile
= await
(alias) namespace cloud
import cloud
cloud
.
namespace cloud_d_exports.fs
export 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, 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 cloud
import cloud
cloud
.
namespace cloud_d_exports.fs
export 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, 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 cloud
import cloud
cloud
.
namespace cloud_d_exports.fs
export 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.

opfs exposes the same supported Node-compatible subset entirely on the device, with no account or network.

import {
(alias) namespace opfs
import opfs
opfs
} from '@fkn/lib'
await
(alias) namespace opfs
import opfs
opfs
.
index_d_exports.mount(): Promise<void>
export index_d_exports.mount
mount
()
(alias) namespace opfs
import opfs
opfs
.
index_d_exports.writeFileSync(path: import("node:fs").PathLike, data: WriteData, options?: WriteOptions): void
export index_d_exports.writeFileSync
writeFileSync
('/cache/poster.bin', new
var Uint8Array: Uint8ArrayConstructor
new (elements: Iterable<number>) => Uint8Array<ArrayBuffer> (+6 overloads)
Uint8Array
([255, 216, 255]))

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:

import {
(alias) namespace cloud
import cloud
cloud
} from '@fkn/lib'
const
const pngBytes: Uint8Array<ArrayBuffer>
pngBytes
= new
var Uint8Array: Uint8ArrayConstructor
new (elements: Iterable<number>) => Uint8Array<ArrayBuffer> (+6 overloads)
Uint8Array
([137, 80, 78, 71])
await
(alias) namespace cloud
import cloud
cloud
.
namespace cloud_d_exports.fs
export 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, 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, options?: WriteOptions) => Promise<void>
writeFile
('save/cover',
const pngBytes: Uint8Array<ArrayBuffer>
pngBytes
, {
contentType?: string | undefined
contentType
: 'image/png' })

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:

import {
(alias) namespace cloud
import cloud
cloud
} from '@fkn/lib'
try {
await
(alias) namespace cloud
import cloud
cloud
.
namespace cloud_d_exports.fs
export 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, 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 cloud
import cloud
cloud
.
namespace cloud_d_exports.fs
export cloud_d_exports.fs
fs
.
class fs_d_exports.StorageLockedError
export fs_d_exports.StorageLockedError
StorageLockedError
) {
// shows the first-party unlock card; resolves true once unlocked
if (await
(alias) namespace cloud
import cloud
cloud
.
namespace cloud_d_exports.fs
export cloud_d_exports.fs
fs
.
fs_d_exports.unlock(): Promise<boolean>
export fs_d_exports.unlock
unlock
()) await
(alias) namespace cloud
import cloud
cloud
.
namespace cloud_d_exports.fs
export 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, 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:

import {
(alias) namespace cloud
import cloud
cloud
} from '@fkn/lib'
const {
const accountEnabled: boolean
accountEnabled
,
const appMode: string
appMode
,
const unlocked: boolean
unlocked
} = await
(alias) namespace cloud
import cloud
cloud
.
namespace cloud_d_exports.fs
export cloud_d_exports.fs
fs
.
fs_d_exports.encryption(): Promise<cloud.fs.EncryptionStatus>
export fs_d_exports.encryption
encryption
()
// 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.