Custom storage
Back persistence with cookies, memory, React Native AsyncStorage, or a WalletPersistence of your own.
The manager persists through config.storage, a WalletPersistence. There are
two levels to customise it at:
- A storage driver keeps butr's key layout and swaps where the strings go:
cookies, memory, AsyncStorage. Wrap it with
createWalletStorage. - A
WalletPersistencereplaces the layout entirely: one key, a server, IndexedDB. Implementloadandsave.
See persistence for what is stored and when.
Drivers
A driver is a three-method get / set / remove interface that may be async:
type StorageDriver = {
getItem: (key: string) => MaybePromise<string | null>;
removeItem: (key: string) => MaybePromise<void>;
setItem: (key: string, value: string) => MaybePromise<void>;
};createWalletStorage takes a persistent driver (pool, selection, active id)
and a session driver (the disconnect intent), defaulting to localStorage and
sessionStorage:
import type { WalletManagerConfig } from "@usebutr/core";
import {
createCookieStorageDriver, // cookies: domain, path, sameSite, secure, maxAgeSeconds
createMemoryStorageDriver, // in-memory, no persistence
createWalletStorage,
} from "@usebutr/core";
import { autoDiscovery } from "@usebutr/wallets";
const config: WalletManagerConfig = {
sources: [autoDiscovery()],
storage: createWalletStorage({
keyPrefix: "myapp",
persistent: createCookieStorageDriver({
domain: "example.com",
maxAgeSeconds: 604_800,
path: "/",
sameSite: "lax",
secure: true,
}),
session: createMemoryStorageDriver(),
}),
};With storage set, storageKeyPrefix is ignored; the prefix goes to
createWalletStorage, and readWalletSnapshot needs the same one.
React Native / Expo (AsyncStorage)
Drivers may be async, so any AsyncStorage-like API works. This is the exact
demo-expo-web driver:
import AsyncStorage from "@react-native-async-storage/async-storage";
import type { StorageDriver } from "@usebutr/core";
const asyncStorageDriver: StorageDriver = {
getItem: (key) => AsyncStorage.getItem(key),
setItem: (key, value) => AsyncStorage.setItem(key, value),
removeItem: (key) => AsyncStorage.removeItem(key),
};
export { asyncStorageDriver };Wire it into the config:
import type { WalletManagerConfig } from "@usebutr/core";
import { createWalletStorage } from "@usebutr/core";
import { WalletManagerProvider } from "@usebutr/react";
import { autoDiscovery } from "@usebutr/wallets";
import type { ReactNode } from "react";
import { asyncStorageDriver } from "./async-storage-driver";
const config: WalletManagerConfig = {
sources: [autoDiscovery()],
storage: createWalletStorage({
keyPrefix: "butr-demo",
persistent: asyncStorageDriver,
session: asyncStorageDriver,
}),
};
const WalletProvider = ({ children }: { children: ReactNode }) => (
<WalletManagerProvider config={config}>{children}</WalletManagerProvider>
);AsyncStorage has no true session-storage equivalent. The demo backs both drivers with the same
store and accepts that session entries outlive the session. butr's session slot holds only the
isUserDisconnected flag, which the next connect clears, and butr itself never reads it to decide
anything. If your own auto-connect logic depends on it, back session with
createMemoryStorageDriver() instead, which resets on every app start.
Your own WalletPersistence
For anything the driver layout doesn't fit, implement the interface directly.
The manager hands save the complete state after every change, so an
implementation never merges, diffs, or queues:
import type { PersistedWalletState, WalletPersistence } from "@usebutr/core";
const EMPTY: PersistedWalletState = {
activeConnectorId: null,
isUserDisconnected: false,
pool: {},
selection: {},
};
const storage: WalletPersistence = {
load: async () => (await db.get<PersistedWalletState>("wallets")) ?? EMPTY,
save: async (state) => {
await db.put("wallets", state);
},
};loadruns once, when the manager starts. A rejection is reported throughonStorageErrorand treated as empty storage, so validate what you read rather than trusting it.savenever runs beforeloadhas resolved, and saves never overlap: while one is in flight, later changes collapse into one follow-up call with the latest state.- A rejected
saveis reported throughonStorageErrorand does not affect the in-memory state. readWalletSnapshotparses butr's own key layout, so server rendering from a custom layout means building theWalletSnapshotyourself.
Source: apps/demo-expo-web/src/async-storage-driver.ts,
apps/demo-expo-web/src/wallet-provider.tsx, and packages/core/src/storage in the butr
repository. See also persistence
concepts.