'worker' (default)
Thread: Dedicated Web Worker. Storage: OPFS.
Production web apps. Keeps the UI responsive while the daemon runs.
The embedded daemon persists two kinds of browser-local data:
swapDatabaseFileName in
RuntimeConfig to name this SQLite file explicitly; set disableSwaps
to turn off the Lightning swap subsystem (and its storage) entirely. When
swaps are disabled, swap endpoint settings (from the network preset or your
overrides) are omitted from the daemon config.OPFS (Origin Private File System) is the production path. Files live under
the page origin in a private directory the page cannot enumerate from outside
the API. The shipped WASM daemon stores the encrypted seed and SQLite databases
in OPFS. Set dataDir in RuntimeConfig to choose a subdirectory (for
example '/my-wallet'); when unset the daemon uses its default.
localStorage is a window-only key/value store. It is not available inside Web Workers, so the default worker transport cannot use it for daemon storage. Main-thread mode can fall back to storage patterns that do not require OPFS, but you lose durable SQLite persistence and the UI thread blocks while the daemon runs.
Your app may still use localStorage for app-level markers (for example recording whether a wallet was created with a passkey or storing a passkey credential id for scoped unlock). The reference demo does this; daemon state remains in OPFS.
OPFS-backed SQLite requires SharedArrayBuffer, which browsers only expose
on cross-origin isolated pages.
Without isolation, OPFS persistence for the daemon will not come up cleanly.
Request persistent storage (navigator.storage.persist()) if you want the
browser to deprioritize eviction of origin data. It is best-effort and does not
replace backing up the recovery phrase.
// Ask the browser to treat this origin's storage as persistent.const granted = await navigator.storage.persist();Whether the browser grants the request depends on its own engagement heuristics (installed as a PWA, bookmarked, frequently visited, and similar signals). Do not rely on it being granted; treat it as best-effort.
Separately from wallet data, the SDK keeps a copy of the daemon’s WASM binary in
Cache Storage, in a bucket named wavelength-runtime-v1-<version>, where the
trailing segment is the RUNTIME_MANIFEST_VERSION the binary belongs to. This is
a performance cache and holds no wallet state.
It exists because the browser will not keep the module in its HTTP cache. The
binary is around 20 MB compressed, which is large enough that Chrome declines to
store it even when it is served with Cache-Control: immutable and hits a CDN
edge, so without a cache of our own every single page load re-downloads the whole
thing. Once the bytes are local, reading them back, verifying the cached module’s
digest, compiling, and instantiating it takes on the order of 165 ms; hashing and
compiling cost about the same as each other, and the read rather less
(re-verifying hashes the full decompressed module on every read; see
Integrity verification),
so on a 50 Mbps connection caching takes the runtime load from roughly 3.2 s to
around 165 ms.
The cache holds one runtime at a time. It is keyed on
RUNTIME_MANIFEST_VERSION, not on the asset URL, so upgrading the SDK to a
runtime with a new version looks in a bucket that does not exist yet, refetches,
and drops the previous version’s bucket on the first load after the upgrade. That
holds however you host the files: a versioned asset path is a good idea for
browser caching, but it is your choice, and the runtime cache does not depend on
it. A wallet that has been through several upgrades holds one copy, not one per
release. Budget roughly 130 MB of origin storage for it, since what is stored is
the decompressed module.
Pass runtimeCache: false to createWebClient to take every runtime binary
from the network instead. That is worth knowing about when you are iterating on
a local daemon build: a rebuild at the same RUNTIME_MANIFEST_VERSION produces
new bytes under a bucket name that has not changed, so the cached copy keeps
being served, and the browser offers no way out of this on its own. DevTools’
Disable cache governs the HTTP cache and leaves Cache Storage alone; only
Application → Storage → Clear site data removes the entry.
Turning the option off deletes nothing. An existing bucket is left exactly as it is and simply not read or written, so turning it back on resumes from it.
Nothing here is load-bearing. Cache Storage is unavailable outside a secure context and rejects writes once an origin is over quota, and the browser may evict the entry at any time. Every one of those cases falls back to downloading the runtime, which is the behavior you had before the cache existed. If the origin is evicted or the entry turns out not to be usable, the SDK drops it and refetches, so a bad cached copy cannot wedge the wallet.
Requesting persistent storage, as above, also makes the browser less likely to evict this cache.
createWebClient() accepts runtimeThread: 'worker' (default) or
'main'.
Thread: Dedicated Web Worker. Storage: OPFS.
Production web apps. Keeps the UI responsive while the daemon runs.
Thread: Page main thread. Storage: No OPFS SQLite path.
Escape hatch when Workers are unavailable or you cannot set COOP/COEP headers. Expect UI jank.
Worker mode spawns the bundled wavewalletdk-worker.js (override with
workerURL). The worker receives runtimeBaseUrl on init so it can fetch
wavewalletdk.wasm.gz, wasm_exec.js, and the SQLite bridge from your hosted asset
folder. Main-thread mode loads those scripts on the page directly.
import { createWebClient, defaultConfig } from '@lightninglabs/wavelength-web';
// Default: worker + OPFS (requires cross-origin isolation).const workerClient = createWebClient({ runtimeBaseUrl: 'https://your-host/wavewalletdk/',});
// Escape hatch: main thread, no OPFS persistence.const mainClient = createWebClient({ runtimeThread: 'main', runtimeBaseUrl: 'https://your-host/wavewalletdk/',});
// Choose a dataDir subdirectory for the daemon's on-disk tree.const config = defaultConfig('signet', { dataDir: '/my-wallet' });await workerClient.ready();await workerClient.start(config);Persistence requirements for the default worker path:
crossOriginIsolated is true and
SharedArrayBuffer is available.RUNTIME_ASSET_FILES) at runtimeBaseUrl.dataDir per wallet profile; changing it starts a fresh
on-disk tree.Wiping local wallet data means clearing both OPFS entries under your origin and
any app-level localStorage keys you wrote. See the demo’s wipe helper
(apps/web-wallet-demo/src/lib/wipeLocalData.ts) for a reference pattern, and
the wavelength-web reference for the client
start/stop lifecycle.