Use this guide with Wallet backup and recovery.
BRC-38 describes a single user's portable wallet records. BRC-39 encrypts that
payload in a password-protected file. Neither supplies root-key recovery.
These are wallet implementation/storage APIs, not new methods on the BRC-100
WalletClient interface.
| Authority | Where to inspect |
|---|---|
| Plaintext record format | BRC-38 |
| Encrypted file format | BRC-39 |
| Incremental provider synchronization | BRC-40 |
| Current helper signatures, validation and import semantics | Portable storage implementation |
| Executable SQLite export/restore/merge and rejection examples | Portable storage tests |
| Public versions and migration decisions | Package migration ledger |
Verified against TS Stack main b2f1989306b6f64677deff89f39596491e448d97
on 2026-09-24. Recheck the implementation and installed declarations when
upgrading. Pending sync work, including
#569, is not an available
API contract for this guide.
PR #569 now captures BRC-38 source metadata, user and all standard table reads inside one provider read view. SQLite uses one transaction; MySQL explicitly requests repeatable-read isolation; IndexedDB uses one readonly transaction covering settings and the wallet stores. SQLite WAL permits an independent writer during capture. IndexedDB queues overlapping writers until capture ends; this checkpoint does not claim bounded foreground write latency for that phase. A local MySQL 8.4.11 fixture with independent connections verifies source isolation, enforced read-only access, unchanged session defaults and failure cleanup. This is not deployed-provider or PXC recovery qualification.
Pass { requireSnapshot: true } to exportBRC38, exportBRC38Json or the
exportBRC39 options to require a coherent source view. Custom StorageProvider
implementations opt in with supportsReadSnapshot() and readSnapshot(callback)
and must honor the token on every query. A required but unsupported snapshot
is refused before table capture. Without that option, old custom providers retain
the documented caller-quiesced legacy path; it does not gain a snapshot guarantee.
The callback is for database capture only, without peer I/O or progress handlers.
Source JSON history omits null/undefined values only for recognized optional
object properties in a detached copy, retaining false, zero, empty strings and
every array position. Required values remain subject to validation. A null array
entry is rejected rather than dropped. The source records are not rewritten.
The candidate also adds a local SQL openReadSnapshot lifetime with explicit
close/cancellation/expiry and one read at a time. It retains a pinned transaction
across idle periods and occupies a pool connection until physical cleanup.
IndexedDB and remote clients do not gain this capability. The existing export
helpers continue to use their scoped capture path; see
retained SQL view limits.
This is an unpublished source candidate. The legacy materialized helpers below still allocate the full document/file. The optional streaming entries described below provide component limits; remote snapshot handles, durable staged recovery, platform adapters and push/backup scheduling remain required parts of the full implementation program.
The 2.15 candidate adds @bsv/wallet-toolbox/portable for bounded canonical value/row projection, the all-thirteen-table BRC-38 encoder and private JSON staging reader, and BRC-39 framing. @bsv/wallet-toolbox/portable/node additionally provides openBrc38KnexSource, native AES-GCM encryption/decryption and createBrc39NodeFileQuarantine. The existing materialized helpers and browser/mobile roots retain their contracts.
Supply explicit row, page, metadata, chunk, archive, password and KDF ceilings. openBrc38KnexSource requires a dedicated compatible SQLite/MySQL reader provider and keeps source settings, identity, sync state and all thirteen table streams in one retained read view. A slow consumer occupies that provider until cleanup; use an independent foreground provider. The built-in SQL source validates closure in that same view before and after complete traversal. Host-defined sources must implement their own independent complete semantic/provenance validation and awaited cleanup.
Encryption emits the existing WDAT envelope with canonical Argon2id defaults (7 iterations, 131072 KiB, parallelism 1), fresh 32-byte salt/nonce, NFC password bytes and a 16-byte GCM tag. New exports refuse weaker strength. Decryption admits valid legacy parameter values only within caller-selected work ceilings and supported native nonce lengths; the materialized codec remains available with its existing input contract. Progress and cancellation occur between owned operations; an already running KDF or file/database operation settles before cleanup. Source validation and physical close must succeed before the final encryption tag. Output remains private until the host completes its durable save transaction.
Decryption writes only into isolated quarantine. GCM authentication must precede strict UTF-8 and complete BRC-38 semantic validation. createBrc39NodeFileQuarantine uses a caller-owned trusted parent, private 0700/0600 files, bounded serial reads/writes, fsync, content verification and explicit awaited discard(). Its withAuthenticatedChunks callback is available after validation and closes its reader even on early return. Always discard after the host operation settles. It does not import, activate a profile or provide a durable recovery transaction. readBrc38JsonStream likewise accepts authenticated plaintext and awaits one private staging callback at a time; its host validator must check every staged row, unique key, relation, identity, network and original provenance before a result can be used.
The BRC-39 frame returns owned ciphertext buffers, including when input comes from Node Buffers or Buffer subviews. Callers may reuse their input chunks after accept() returns; changing emitted bytes does not alter the frame's retained tag or later output.
These entry points bound each component's admitted work and buffers. They do not establish native allocator/RSS/IPC bounds, hard database/WAL/directory quotas, durable occupied-target restore, replicated remote export destinations, or physical mobile qualification. Those remain mandatory in the full #544 program. No pending intermediate API is a released production guarantee.
A selected host Argon2id backend must return a fresh owned 32-byte key buffer for each streaming operation. The Node streaming adapter consumes and wipes that buffer after success or failure; a backend must not reuse it between calls.
The implementation exports one user, its sourceStorage metadata and 13
table arrays: transactions, outputs, output baskets, output tags and maps,
transaction labels and maps, certificates and fields, commissions, linked
proven transactions and proof requests, and sync states. It retains row
relationships, timestamps, tombstones and binary data. This is different from
the 12 entity kinds used in the storage sync protocol; do not use a sync-store
count as the archive schema.
The export does not contain root keys, unrelated manager snapshots, other profiles, storage-global monitor events, or product data held outside these tables. Inventory contacts, permissions, external files and custom signing dependencies in your product before describing its backup coverage.
This exclusion also applies to application-owned tables in the same database: reports, sessions and other custom tables are not discovered or copied by the portable helpers. Preserving an output does not preserve its application meaning when that meaning exists only in a custom table. Use standard baskets, tags, labels and their relationships where their semantics fit; otherwise provide a separately versioned application export/import and test its links to restored outpoints. Distinguish durable user content from disposable sessions, caches and credentials, which must not be copied indiscriminately. A successful wallet import must disclose any missing application context.
For an older source wallet, check its installed exports and schema before planning a drill. If the required helpers are absent, preserve the original and qualify an isolated copy through the supported migration/export path. Record both versions; importing a file on the candidate does not prove that an older wallet can produce that file unchanged.
The released helpers read multiple tables and materialize the complete document
and encrypted file in memory. They do not take a database-wide snapshot,
provide streaming archive I/O, expose an archive progress/cancellation API, or
promise a coherent view while other writers change the source. Use a stable
local replica or a database-consistent restored copy. Quiesce its writers and
monitor work while exporting. A lock on one WalletStorageManager does not
stop other processes or devices from writing.
The functions require a concrete StorageProvider, such as StorageKnex or
StorageIdb, not a remote StorageClient/StorageMobile or an arbitrary
WalletStorageProvider. For a remote wallet, first synchronize the selected
identity into a compatible local provider through supported storage APIs,
verify that copy, then export it. Do not cast a remote client to
StorageProvider or expose unrestricted database methods to make an example
compile. For mobile, choose and qualify a compatible local provider or a
separate trusted export path; a mobile package export does not by itself add
local persistent storage.
WalletStorageManager.syncFromReader(identityKey, reader) and
updateBackups() are available building blocks. Check their completion/error
results and independently verify coverage before declaring the local copy
complete. With a live changing source, a successful transfer alone is not a
point-in-time consistency guarantee. See the
storage reference.
A local synchronized copy has its own storage identity and sync bookkeeping. Compare wallet data semantically and record that provenance; do not describe its export as a byte-for-byte copy of the original provider's database.
These public changes provide host-integration examples to inspect alongside this guide. Status checked on 2026-09-24; an open or merged PR is not evidence that a store-distributed application version has shipped or passed your drill.
Use synthetic data and record both implementations' exact versions for cross-wallet tests. These examples do not establish universal compatibility.
The Node package exports the following helpers at @bsv/wallet-toolbox.
Browser and mobile roots export the portable helpers through
@bsv/wallet-toolbox-client and @bsv/wallet-toolbox-mobile respectively;
use the package appropriate to your runtime and qualify its provider/KDF path.
| Helper | Result or purpose |
|---|---|
exportBRC38(storage, identityKey) | Validated BRC38WalletData object |
exportBRC38Json(storage, identityKey) | Canonical plaintext JSON string; highly sensitive |
parseBRC38Json(json) | Parses and validates plaintext records |
encryptBRC39(documentOrJson, password) | Encrypts an existing BRC-38 dataset into number[] bytes |
exportBRC39(storage, identityKey, password) | Exports and encrypts in one operation |
decryptBRC39(bytes, password) | Authenticates, decrypts and validates before returning records |
importBRC38(storage, documentOrJson, { mode }) | Writes validated records using explicit restore/merge semantics |
importBRC39(storage, bytes, password, { mode }) | Decrypts and imports; no selected-profile confirmation UI |
Use the library's cryptographic defaults. BRC-39 files conventionally use
wallet.brc39 and media type application/vnd.brc39.wallet. Preserve the bytes;
do not stringify the numeric array and label it a BRC-39 file. Do not trim or
case-fold the passphrase. The helper performs the required NFC normalization.
Its default Argon2id memory cost alone is 128 MiB, in addition to the archive
and runtime allocations. Test realistic files on the lowest supported device.
This TypeScript function uses real public APIs. The caller must supply a quiesced, verified local data copy for the selected authenticated profile. File pickers, secret entry, disk permissions and download completion are host application responsibilities; the function does not persist a file.
import { exportBRC39, type StorageProvider } from '@bsv/wallet-toolbox'
export async function makeWalletDataFile(
source: StorageProvider,
selectedIdentityKey: string,
expectedChain: string,
passphrase: string
): Promise<Uint8Array> {
const settings = await source.makeAvailable()
if (settings.chain !== expectedChain) throw new Error('Wrong source network')
const bytes = await exportBRC39(source, selectedIdentityKey, passphrase)
return Uint8Array.from(bytes)
}Bind identity/network to the selected profile before starting. Keep that selection stable through completion or abort the host operation when it changes. Deliver the encrypted bytes through the platform's file API, retain older known-good copies, and record completion only after the destination save succeeds. Store a file digest and timestamp as private verification metadata; neither proves freshness or completeness by itself. Reopen the saved file and verify it in an isolated recovery drill.
The following functions illustrate API wiring after the host's resource checks; they are not a complete untrusted-file admission layer. Before decryption, the host must bound encoded KDF work/memory parameters as well as file bytes using a reviewed header parser and a device-qualified policy. A small file can request expensive Argon2id work. The current helper checks parameter validity but does not accept a caller-supplied resource ceiling or cancellation signal. Do not weaken parameters or edit the file to fit a device: refuse it with a supported recovery path on a suitably provisioned runtime.
Restore into newly provisioned storage with the correct schema and network.
For mode: 'restore', every data table must be empty, including users and
monitor_events; only settings may exist. A convenience wallet setup can
create a user before import and therefore make the target ineligible. Never
empty a user's existing store to satisfy this precondition.
The caller must already have recovered the selected profile's key and derived its identity independently. Decrypt first to inspect the archive, compare the identity/network, obtain the user's confirmation of the target and mode, then invoke import. Do not trust an archive's identity as the selected profile.
import {
decryptBRC39,
importBRC38,
type BRC38WalletData,
type StorageProvider
} from '@bsv/wallet-toolbox'
export async function previewWalletDataFile(
bytes: Uint8Array,
passphrase: string,
recoveredIdentityKey: string,
expectedChain: string
): Promise<BRC38WalletData> {
const document = await decryptBRC39(bytes, passphrase)
if (document.user.identityKey !== recoveredIdentityKey) {
throw new Error('Archive belongs to a different wallet')
}
if (document.sourceStorage.chain !== expectedChain) {
throw new Error('Archive belongs to a different network')
}
return document
}
// Call only after profile/target confirmation. Do not mutate the preview.
export async function restoreConfirmedWalletData(
emptyTarget: StorageProvider,
document: BRC38WalletData
) {
return await importBRC38(emptyTarget, document, { mode: 'restore' })
}Recheck the selected profile and target immediately before committing; reject a changed selection. Keep the decrypted preview in trusted memory, not logs, analytics or general application state persistence. Import validates network compatibility but does not know the UI's active profile or prove possession of the root key. Import success is data-write evidence, not proof of spendability.
| Mode | Current behavior | Recovery implication |
|---|---|---|
restore | Checks for empty data tables, inserts rows in a storage transaction and retains exported IDs | Use an isolated new target with exclusive access; the emptiness check is not a cross-process lock |
merge | Finds/inserts the archive identity, uses sync processing and remaps IDs/relationships and sync maps | Protect existing state first; this is not whole-database replacement or a single all-or-nothing transaction |
Do not offer merge as an automatic retry after restore rejects a nonempty
target. Make it an explicit, separately tested product flow for the same wallet
identity. Imported active-storage and sync metadata must not silently authorize
a new endpoint or promote a provider. Restore retains the exported user's
activeStorage; the destination retains its own settings. Review authority
and use supported manager operations only after validation. Current import
also normalizes an exact legacy managed-change basket default; the
managed-change guide
and portable tests describe that compatibility behavior.
Bound file size before reading it into memory, concurrent imports and total
work on the supported device. The current helpers do not expose an
AbortSignal; a cancelled UI must not claim that an in-flight write rolled
back. Keep incomplete targets isolated and retain the source/archive for a
controlled retry. Do not lower KDF strength or bypass validation to make an
oversized or malformed file import successfully.
Wrong passwords, damaged authentication tags, unsupported headers and invalid records must leave the active wallet unchanged. Preserve the original file and give a clear retry/support path. There is no passphrase-reset function for an existing BRC-39 file; changing an application login password does not re-encrypt old exports. Use the recovery checklist for failure, large-file and cross-runtime tests. Never upload a real export, passphrase, root key or manager snapshot to an AI agent or public issue.