# How to Configure Swappable Persistence Backends in @openmaic/storage

> Easily configure swappable persistence backends in @openmaic/storage. Implement KVStore and integrate seamlessly with Zustand using BrowserKVStore, HttpKVStore, PgKVStore, or custom solutions.

- Repository: [MAIC/OpenMAIC](https://github.com/THU-MAIC/OpenMAIC)
- Tags: how-to-guide
- Published: 2026-09-02

---

**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](https://github.com/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`](https://github.com/THU-MAIC/OpenMAIC/blob/main/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`** — Uses `localStorage`/`sessionStorage` ([`src/kv/browser.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/src/kv/browser.ts))
- **`HttpKVStore`** — Communicates with a remote HTTP API ([`src/kv/http.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/src/kv/http.ts))
- **`PgKVStore`** — Persists to PostgreSQL ([`src/kv/pg.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/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`](https://github.com/THU-MAIC/OpenMAIC/blob/main/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`](https://github.com/THU-MAIC/OpenMAIC/blob/main/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

```typescript
// 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`:

```typescript
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:

```typescript
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`](https://github.com/THU-MAIC/OpenMAIC/blob/main/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:

```typescript
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:

```typescript
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`](https://github.com/THU-MAIC/OpenMAIC/blob/main/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

```typescript
// 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`](https://github.com/THU-MAIC/OpenMAIC/blob/main/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`](https://github.com/THU-MAIC/OpenMAIC/blob/main/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`](https://github.com/THU-MAIC/OpenMAIC/blob/main/src/zustand/persist.ts) prevents accidental misuse.