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:

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:

  1. Accepts a KVScope ('account' or 'device')
  2. Accepts optional KVPersistDeps including a custom kv backend
  3. Returns a Zustand-compatible PersistStorage adapter
// 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: reportPersistHealth callback for monitoring
  • Legacy cleanup: purgeLegacyPersistKey removes obsolete localStorage entries

These features are implemented in the state machine within src/zustand/persist.ts and require no additional configuration.

Summary

  • KVStore is the minimal interface (get, set, remove, keys) that all persistence backends must implement
  • createKVPersistStorage is the factory that adapts any KVStore to Zustand's persist middleware
  • Pass a custom backend via the kv property in KVPersistDeps to override the default BrowserKVStore
  • Use 'device' scope only with backends where servesDeviceScopeLocally === true to protect sensitive data
  • Swap at runtime by calling createKVPersistStorage with different kv instances 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →