How to Configure Swappable Persistence Backends in @openmaic/storage
Configure swappable persistence backends in @openmaic/storage by implementing the KVStore interface and passing any concrete backend—BrowserKVStore, HttpKVStore, PgKVStore, or a custom implementation—to createKVPersistStorage for seamless Zustand integration.
The @openmaic/storage package from the THU-MAIC/OpenMAIC repository separates state management from persistence through a clean abstraction layer. This design lets you swap storage backends at compile time or runtime without modifying your Zustand store definitions. Whether you need browser-based localStorage, HTTP-backed remote persistence, or PostgreSQL for server-side rendering, the same interface powers them all.
Understanding the KVStore Abstraction
The foundation of swappable persistence is the KVStore interface defined in src/kv/types.ts. This contract requires four methods:
| Method | Purpose |
|---|---|
get(key: string) |
Retrieve a value by key |
set(key: string, value: string) |
Store a key-value pair |
remove(key: string) |
Delete a key |
keys() |
List all stored keys |
Any class implementing these methods becomes a valid backend. The repository provides three built-in implementations:
BrowserKVStore— UseslocalStorage/sessionStorage(src/kv/browser.ts)HttpKVStore— Communicates with a remote HTTP API (src/kv/http.ts)PgKVStore— Persists to PostgreSQL (src/kv/pg.ts)
Device-Safe Stores and Scope Enforcement
The KVStore interface includes a runtime flag servesDeviceScopeLocally that marks backends guaranteeing data never leaves the local machine. This is critical for the 'device' scope, which stores sensitive data like API keys or encryption secrets that must remain client-side.
In src/zustand/persist.ts, the kvPersistStorage function enforces this safety at lines 63–68: attempting to use a non-device-safe backend with 'device' scope throws a runtime error. The 'account' scope has no such restriction.
The Persistence Factory: createKVPersistStorage
The primary entry point for backend configuration is createKVPersistStorage in src/zustand/persist.ts (lines 19–30). This factory:
- Accepts a
KVScope('account'or'device') - Accepts optional
KVPersistDepsincluding a customkvbackend - Returns a Zustand-compatible
PersistStorageadapter
// src/zustand/persist.ts#L19-L30
export function createKVPersistStorage(
scope: KVScope,
deps?: KVPersistDeps
): PersistStorage<unknown> {
const kv = deps?.kv ?? defaultKv; // Falls back to BrowserKVStore
// ... state machine initialization for failure recovery
}
The factory also wires a state machine that handles network failures, retries with exponential backoff (DEFAULT_RECOVERY_BACKOFF_MS), and reports health via reportPersistHealth.
Swapping Backends: Four Common Patterns
1. Default Browser Backend
When no backend is specified, createKVPersistStorage automatically uses BrowserKVStore:
import { createKVPersistStorage } from '@openmaic/storage';
import { persist } from 'zustand/middleware';
import { createStore } from 'zustand';
const storage = createKVPersistStorage('account');
export const useUserStore = createStore(persist(
(set) => ({
username: '',
setUsername: (name) => set({ username: name }),
}),
{ name: 'user-store', storage }
));
2. HTTP-Backed Remote Persistence
Swap to a server-side backend by instantiating HttpKVStore and passing it explicitly:
import { HttpKVStore, createKVPersistStorage } from '@openmaic/storage';
import { persist } from 'zustand/middleware';
import { createStore } from 'zustand';
const httpBackend = new HttpKVStore({
baseUrl: '/api/kv',
// Optional: authToken added after login
});
const httpStorage = createKVPersistStorage('account', { kv: httpBackend });
export const useSettingsStore = createStore(persist(
(set) => ({
theme: 'dark',
setTheme: (theme) => set({ theme }),
}),
{ name: 'settings-store', storage: httpStorage }
));
The HTTP contract is documented in docs/kv-http-contract.md for implementing compatible server endpoints.
3. Device-Scoped Local Storage
For secrets that must never reach remote servers, combine BrowserKVStore with the 'device' scope:
import { BrowserKVStore, createKVPersistStorage } from '@openmaic/storage';
import { persist } from 'zustand/middleware';
import { createStore } from 'zustand';
const deviceStorage = createKVPersistStorage('device', {
kv: new BrowserKVStore()
});
export const useSecureStore = createStore(persist(
(set) => ({
encryptionKey: null as string | null,
setEncryptionKey: (key) => set({ encryptionKey: key }),
}),
{ name: 'secure-store', storage: deviceStorage }
));
BrowserKVStore.servesDeviceScopeLocally === true satisfies the runtime safety check.
4. Runtime Backend Swapping
Because createKVPersistStorage resolves the backend on each call, you can swap implementations dynamically—useful for authentication flows:
import { BrowserKVStore, HttpKVStore, createKVPersistStorage } from '@openmaic/storage';
import type { KVStore, KVScope } from '@openmaic/storage';
// Mutable reference to current backend
let currentKV: KVStore = new BrowserKVStore();
export const authenticateKV = (token: string) => {
currentKV = new HttpKVStore({
baseUrl: '/api/kv',
authToken: token
});
};
export const getPersistStorage = (scope: KVScope) =>
createKVPersistStorage(scope, { kv: currentKV });
// Store definition uses the current backend at mount time
export const useDataStore = createStore(persist(
(set) => ({ /* ... */ }),
{
name: 'data-store',
storage: getPersistStorage('account')
}
));
Application-Level Configuration
For larger applications, the repository provides lib/store/kv-persist.ts as a central configuration point. This file demonstrates how to:
- Resolve the appropriate backend based on environment (SSR, test, production)
- Inject dependencies via
KVPersistDeps.kv - Initialize per-store state machines with custom recovery behavior
// lib/store/kv-persist.ts#L34-L41
export function createKVPersistForKey(
key: string,
deps: KVPersistDeps = {}
) {
const kv = deps.kv ?? resolveDefaultKV(); // Environment-aware resolution
return createKVPersistStorage('account', { kv });
}
Built-In Reliability Features
When you configure a custom backend through createKVPersistStorage, you automatically gain:
- Failure recovery: Exponential backoff on write failures
- Write replay: Queued writes retry once backend recovers
- Health reporting:
reportPersistHealthcallback for monitoring - Legacy cleanup:
purgeLegacyPersistKeyremoves obsoletelocalStorageentries
These features are implemented in the state machine within src/zustand/persist.ts and require no additional configuration.
Summary
KVStoreis the minimal interface (get,set,remove,keys) that all persistence backends must implementcreateKVPersistStorageis the factory that adapts anyKVStoreto Zustand'spersistmiddleware- Pass a custom backend via the
kvproperty inKVPersistDepsto override the defaultBrowserKVStore - Use
'device'scope only with backends whereservesDeviceScopeLocally === trueto protect sensitive data - Swap at runtime by calling
createKVPersistStoragewith differentkvinstances as conditions change
Frequently Asked Questions
What methods must a custom KVStore implementation provide?
Your custom backend must implement four methods from src/kv/types.ts: get(key) returning Promise<string | null>, set(key, value) returning Promise<void>, remove(key) returning Promise<void>, and keys() returning Promise<string[]>. For device-scoped stores, also set servesDeviceScopeLocally = true.
Can I use @openmaic/storage without Zustand?
Yes, though the primary integration is Zustand's persist middleware. You can use KVStore implementations directly for any key-value needs. The kvPersistStorage adapter simply bridges to Zustand's expected PersistStorage shape.
How do I handle backend failures gracefully?
The createKVPersistStorage factory automatically wraps your backend in a state machine that implements exponential backoff (DEFAULT_RECOVERY_BACKOFF_MS), write queuing, and health reporting via reportPersistHealth. No manual intervention required.
What's the difference between 'account' and 'device' scope?
'account' scope stores data that can sync across devices and uses any KVStore. 'device' scope stores machine-local secrets (encryption keys, hardware IDs) and requires a backend with servesDeviceScopeLocally === true. The runtime check in src/zustand/persist.ts prevents accidental misuse.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →