Storage ProvidersNode Providers

Node Providers

The filesystem backends — FileProvider from the package root and HybridProvider / HybridProviderV3 from wative-core/node (V3 derives one key per workspace) — and why a provider must come from the same entry point as Workspace.

On the server, a workspace lives in a directory: four binary singletons (config, network, asset, log) plus one <slug>.db blob per account under accounts/. Two providers write that layout — HybridProvider and HybridProviderV3 — and both come from the Node-only entry point wative-core/node. They are the filesystem backends Workspace.open() selects by default in Node, so a path is usually all you need:

node-default.ts
import { Workspace } from "wative-core";
import "wative-core/node"; // registers the filesystem backends (side effect)

const ws = await Workspace.open({ path: "./my-wallet", password: "wsp-pwd" });
await ws.lock();

HybridProvider

HybridProvider is the launch filesystem backend. It seals every blob with the v2 envelope — Argon2id (vendored WebAssembly) plus AES-256-GCM with identity-bound AAD — and writes atomically (write to a temp file, fsync, rename), with symlink-root refusal and Windows long-path handling. It also reads v3 containers; the old v1 (PBKDF2) format was removed and is no longer readable by any build in this major.

hybrid.ts
import { Workspace, HybridProvider } from "wative-core";

const ws = await Workspace.open(new HybridProvider("~/wallets"), "wsp-pwd");
await ws.lock();

The constructor takes a root path (a leading ~, ~/ or ~\ is expanded, then the path is resolved to absolute) and exposes it as the read-only rootPath. A non-string or empty path throws WativeError (code === "PARAMETER_ERROR"). A static HybridProvider.probe(rootPath): Promise<boolean> reports whether a directory already looks like a wative container (it holds a config file or an accounts/ directory).

HybridProviderV3

HybridProviderV3 extends HybridProvider and writes the stronger v3 envelope through native @node-rs/argon2 bindings. Where v2 derives an Argon2 key directly for every record, v3 derives one key-encryption key per container and expands each record's key from it with HKDF, with explicit domain separation per record.

hybrid-v3.ts
import { Workspace, HybridProviderV3 } from "wative-core";

const ws = await Workspace.open(new HybridProviderV3("~/wallets"), "wsp-pwd");
await ws.lock();

Why V3 is faster

v2's unlock cost is linear in the number of secrets a container holds; v3's is constant. Measured at production parameters, unlocking a 52-address account costs one Argon2 derivation instead of 53 — roughly 145 ms instead of 7.6 s — and that figure does not grow with the account.

HybridProvider (v2)HybridProviderV3 (v3)
Key derivationOne Argon2 derivation per recordOne per container, HKDF per record
Unlock costLinear in secrets heldConstant
Argon2 backendVendored WebAssemblyNative @node-rs/argon2
Runs inNode and browserNode only

Reads are unaffected by the choice: every record carries its own version byte, so HybridProviderV3 opens a v2 container and HybridProvider opens a v3 one.

The Node default and @node-rs/argon2

@node-rs/argon2 is an optional dependency — a few platforms have no prebuilt binary — so the Node default cannot simply require it. When you pass a path (or nothing) to Workspace.open(), the default is HybridProviderV3 where the native binding can be built, and HybridProvider otherwise:

  • The HybridProviderV3 constructor resolves @node-rs/argon2 eagerly, so a missing or unbuildable binary is reported at construction — where the message can name the package — rather than mid-unlock, after a workspace directory already exists.
  • The default silently falls back to HybridProvider if that construction throws. A machine that falls back reads a v3 container fine and writes v2 from then on.
  • Which format a new workspace gets therefore depends on whether the optional native dependency installed. argon2BackendInfo() (exported from wative-core) reports what was resolved.
pnpm add @node-rs/argon2
npm install @node-rs/argon2

FileProvider

FileProvider is exported from the package root and is reserved for a future flat-binary-blob backend (local file / SFTP / NAS). It is a placeholder in this release: every method — including the constructor — throws a WativeError with code === "UNSUPPORTED_OP" and the message "not implemented in v2.0; use HybridProvider". Do not build on it yet.

The node subpath and the entry-point rule

wative-core/node is the Node-only surface. Importing it registers the filesystem backends as a side effect — that is what lets Workspace.open() accept a path (or no argument) in Node. It exports HybridProvider, HybridProviderV3, FileSink and resolveDefaultWorkspacePath. Because a browser bundler resolves the package's "browser" condition instead, it never pulls this module in, which is what keeps node:fs out of a web build.

For backward compatibility, HybridProvider and HybridProviderV3 are also re-exported from the package root in Node (this re-export is deprecated; prefer wative-core/node, or the root re-export shown throughout this page).

The catch is the same-entry-point rule: Workspace.open() refuses a provider built by a different bundle, because each entry point is a self-contained bundle with its own class identities and module state. Since Workspace is exported from wative-core (not from wative-core/node), the way to hand Workspace.open() an explicit filesystem provider is to take both from wative-core:

same-entry.ts
// ✅ Both from wative-core.
import { Workspace, HybridProviderV3 } from "wative-core";
const ws = await Workspace.open(new HybridProviderV3("~/wallets"), "wsp-pwd");
mixed-entry.ts
// ❌ Rejected with PARAMETER_ERROR — Workspace and the provider are different bundles.
import { Workspace } from "wative-core";
import { HybridProviderV3 } from "wative-core/node";
await Workspace.open(new HybridProviderV3("~/wallets"), "wsp-pwd");

Last updated on