Storage Providers
How a Workspace persists encrypted records, which backend Workspace.open() selects per environment (HybridProviderV3/HybridProvider in Node, IdbProvider in the browser), and when to pass a provider explicitly.
A Workspace never touches storage directly. Every account, network, asset, config and log entry is an encrypted record, and a provider is the thing that moves those sealed bytes to and from a backing store — a directory on disk, an IndexedDB database, or anything you write yourself. The provider owns the store; the Workspace owns the wallet logic on top of it.
Most callers never name a provider. Workspace.open() picks a sensible default for the environment it is running in, so a path (or nothing at all) is enough:
import { Workspace } from "wative-core";
// Node: a filesystem workspace at ./my-wallet
const ws = await Workspace.open({ path: "./my-wallet", password: "wsp-pwd" });
await ws.lock();You reach for an explicit provider only when the default does not fit — a custom store, or a browser workspace that needs to negotiate durable storage before it is created.
Which backend Workspace.open() selects
When you do not pass a provider, the backend is chosen by environment. In Node it is registered by importing wative-core/node (a side effect of the import); in the browser it is registered by wative-core itself, but only where indexedDB actually exists.
| Environment | Default backend | Notes |
|---|---|---|
Node, @node-rs/argon2 available | HybridProviderV3 | Filesystem. Derives one key per workspace, so unlocking a many-address account costs a single derivation. |
| Node, native Argon2 unavailable | HybridProvider | Filesystem. Falls back automatically; every record carries its own version, so either provider opens the other's container. |
Browser (where indexedDB exists) | IdbProvider | IndexedDB, created with the name "wative". Storage is evictable, so creating a new workspace here is gated — see below. |
The optional first argument to Workspace.open() is interpreted per environment: it is a filesystem path in Node and a database name in the browser, so Workspace.open("my-wallet", pwd) reads naturally in both. Options that need a decision — acknowledging evictable storage, injecting an IndexedDB factory — are not reachable through the default, so construct an IdbProvider yourself for those.
In Node, when no path is given either, the location is resolved in three tiers: WATIVE_WORKSPACE_PATH (if set and non-empty) always wins, then <cwd>/.wative2 if it already exists, then <home>/.wative2 as the last-resort fallback.
The same-entry-point rule
The package publishes several entry points, and each is a self-contained bundle with its own class identities and module state. A provider built by one bundle and handed to a Workspace from another fails the internal instanceof check, and Workspace.open() refuses it with a WativeError whose code is "PARAMETER_ERROR":
import { Workspace } from "wative-core";
import { HybridProviderV3 } from "wative-core/node"; // ❌ different bundle
// Throws PARAMETER_ERROR: the provider came from a different entry point.
await Workspace.open(new HybridProviderV3("~/wallets"), "wsp-pwd");Import Workspace and the provider you construct from the same specifier:
import { Workspace, HybridProviderV3 } from "wative-core";
const ws = await Workspace.open(new HybridProviderV3("~/wallets"), "wsp-pwd");
await ws.lock();This only matters when you construct a provider instance yourself. If you let Workspace.open() build the default — by passing a path, a name, or nothing — there is no mixing to get wrong. See Node providers for the full explanation of why Workspace is exported from wative-core while the filesystem classes are canonical on wative-core/node.
Create vs. open: inspectContainer and ContainerState
Workspace.open() has no create flag (the old third positional argument is deprecated and ignored). Instead it inspects the target's structure before unlocking and decides:
ContainerState | Meaning | What open() does |
|---|---|---|
"empty" | No container yet | Creates a new workspace there. |
"workspace" | An existing wative workspace | Opens it and verifies the password. |
"foreign" | Non-empty storage that is not a wative workspace | Refuses with PARAMETER_ERROR, so a mistyped path never scaffolds a workspace on top of unrelated data. |
ContainerState is a public type ("empty" | "workspace" | "foreign") and Provider.inspectContainer() returns Promise<ContainerState>. The base Provider default returns "workspace" — the never-refuse answer — so a minimal custom provider is unaffected. Providers that can cheaply inspect their store override it: ContainerProvider implements a generic three-state check over its storage primitives, and HybridProvider detects a "foreign" filesystem directory.
No password? Workspace.open() skips inspection entirely and returns a locked workspace; drive ws.unlock(pwd) when you are ready. See Workspace.open().
Choosing a base class to extend
If a built-in backend does not cover your store, you write one by subclassing. There are two extension points, and the right one depends on whether your store already has its own encryption:
The built-in providers
See also Records for the Record handle a provider hands back, and Environments for how the Node and browser entry points differ.