Records
The Record layer every Workspace persists — reading records off a provider, the locked/unlocked distinction, value/unlock()/save(), and why a Record handle does not outlive its session.
Underneath every Workspace is a Provider, and a provider frames its storage as encrypted records. Each record is one sealed unit — the config, the network list, the asset list, the logger config, or one account — read and written through the Record<T> handle the provider hands back. This is the persistence layer most code never touches directly; it is exposed for custom-provider authors and for tools that need to work with the store below the Workspace API.
Record and Provider are exported from wative-core. Provider itself is abstract — you obtain records from a concrete provider such as HybridProvider or IdbProvider (see Providers), or from the one a Workspace builds internally.
Record shape
class Record<T> {
readonly slug: Slug;
readonly path: string;
get locked(): boolean;
get value(): T; // throws RECORD_LOCKED while locked
unlock(password: string): this;
save(): Promise<void>;
}| Member | Kind | Notes |
|---|---|---|
slug | Slug | The record's key within its type. |
path | string | Where the record lives in the backing store. |
locked | getter | true until the record has been decrypted. |
value | getter | The decrypted payload of type T. Throws RECORD_LOCKED if the record is still locked. |
unlock(password) | method | Decrypts the record with password and returns this. |
save() | method | Re-encrypts the current value and writes it back through the owning provider. |
Reading records off a provider
A provider exposes two read methods, both returning Record handles:
// One record: `slug` is required for the ACCOUNTS type, optional otherwise.
const record = await provider.loadRecord<WorkspaceConfig>("CONFIG");
// Every record of a type.
const accounts = await provider.loadRecords("ACCOUNTS");| Method | Returns |
|---|---|
loadRecord<T>(type, slug?) | Promise<Record<T>> |
loadRecords<T>(type) | Promise<ReadonlyArray<Record<T>>> |
A record that is genuinely absent is reported as a WativeError with code === "RECORD_NOT_FOUND" — that is part of the provider contract, and it is how the library tells "not there" apart from "there but unreadable".
The locked / unlocked distinction
loadRecord applies the container password eagerly, so a record it can open with that password comes back already unlocked — record.locked is false and record.value returns the payload straight away.
A record the container password does not open comes back locked instead of throwing — a record sealed under a different password, or a damaged one. Reading .value on it throws:
const record = await provider.loadRecord("ACCOUNTS", slug);
if (record.locked) {
// record.value here would throw RECORD_LOCKED
record.unlock(otherPassword); // decrypt with the record's own password
}
record.value; // now safe to readunlock() is an active credential check: a wrong password throws BAD_PASSWORD, and a record with no ciphertext to verify against throws DECRYPT_FAILED. Those two are distinguishable — "wrong password" versus "cannot be checked".
save() re-encrypts
save() seals the current value again and persists it through the owning provider, under the record's own type and slug. It refuses on a record that is still locked — there is nothing decrypted to write — throwing a WativeError with code === "RECORD_LOCKED".
const record = await provider.loadRecord<WorkspaceConfig>("CONFIG");
// mutate record.value ...
await record.save(); // re-encrypts and writes backA record handle does not outlive its session
A Record is bound to the container session that issued it. Session-backed providers install a liveness check that unlock() and save() consult first, so a handle you hold across a session boundary refuses rather than writing into a store it no longer belongs to:
- After the workspace is locked (and possibly unlocked again into a new session),
unlock()/save()throwWORKSPACE_LOCKED— the session that minted the handle has closed. Load the record again from the current session. - If the underlying record was dropped after the handle was loaded, they throw
RECORD_NOT_FOUND— saving would otherwise recreate a deleted record, key material and all.
value is deliberately not gated by the liveness check — it is a snapshot the caller already holds, so reading it off a stale handle still returns what it decrypted. Only the credential check (unlock) and the write (save) refuse across a session boundary. Do not keep Record handles past a lock(); re-load them after each unlock().
RecordType
Every record belongs to one of five types:
type RecordType = "CONFIG" | "ACCOUNTS" | "NETWORK" | "ASSET" | "LOG";| Type | Holds |
|---|---|
"CONFIG" | Workspace-wide settings (see Workspace Config). |
"ACCOUNTS" | Per-account records — one per account, keyed by slug. |
"NETWORK" | The user-added network list. |
"ASSET" | The asset list (built-in plus imported). |
"LOG" | The logger configuration. |
The ACCOUNTS type is the only one keyed by slug (one record per account); the other four are singletons. Absence of any singleton is re-seeded from the bundled templates when the workspace initializes.