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:
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.
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).
HybridProvider is single-process. Opening the same root from two Node processes at once is unsupported and can lose data — no advisory lockfile is taken.
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.
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 derivation | One Argon2 derivation per record | One per container, HKDF per record |
| Unlock cost | Linear in secrets held | Constant |
| Argon2 backend | Vendored WebAssembly | Native @node-rs/argon2 |
| Runs in | Node and browser | Node 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.
A v3 container is not readable by older library versions — wative-core 2.x knows only v1 and v2. Adopting v3 is a one-way door with respect to the library version (there is no downgrade), though not with respect to the provider: within this version either provider opens the other's container.
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
HybridProviderV3constructor resolves@node-rs/argon2eagerly, 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
HybridProviderif 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 fromwative-core) reports what was resolved.
pnpm add @node-rs/argon2npm install @node-rs/argon2FileProvider
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.
For a filesystem workspace today, use HybridProvider or HybridProviderV3. new FileProvider(...) throws immediately. (The database placeholder DbProvider behaves the same way.)
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:
// ✅ Both from wative-core.
import { Workspace, HybridProviderV3 } from "wative-core";
const ws = await Workspace.open(new HybridProviderV3("~/wallets"), "wsp-pwd");// ❌ 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");If you do not construct a provider yourself, this cannot go wrong: import "wative-core/node" for its registration side effect and pass a path (or nothing) to Workspace.open() — it builds the correct default internally. See the same-entry-point rule and Environments.