Skip to content

@zod-vault/zustand

The main package — encrypted persistence middleware for Zustand.

Middleware that adds encrypted persistence and cloud sync.

import { create } from "zustand";
import { vault } from "@zod-vault/zustand";
const useStore = create(
vault(
(set, get) => ({
count: 0,
increment: () => set((s) => ({ count: s.count + 1 })),
}),
{
name: "my-store",
recoveryKey: "ABCD-EFGH-...",
}
)
);
OptionTypeRequiredDefaultDescription
namestringYes-Unique identifier for this vault
recoveryKeystringYes-Encryption key
serverstringNo-Server URL for cloud sync
getToken() => string | nullNo*-Auth token getter (*required if server set)
partialize(state) => partialNo(s) => sSelect which state to persist
merge(persisted, current) => mergedNoObject.assignHow to merge persisted state
skipHydrationbooleanNofalseSkip auto-hydration on init
syncIntervalnumberNo30000Auto-sync interval (0 to disable)
storageVaultStorageNolocalStorageCustom storage implementation
prefixstringNozod-vault:Storage key prefix
onRehydrateStorage(state) => callbackNo-Hydration lifecycle hook

The middleware adds a vault object to the store:

useStore.vault.sync()
useStore.vault.push()
useStore.vault.pull()
useStore.vault.rehydrate()
useStore.vault.hasHydrated()
useStore.vault.getSyncStatus()
useStore.vault.hasPendingChanges()
useStore.vault.clearStorage()
useStore.vault.onHydrate(callback)
useStore.vault.onFinishHydration(callback)

Full bidirectional sync with server.

await useStore.vault.sync();

Pushes local changes, pulls remote changes, merges with LWW.

Push local state to server.

await useStore.vault.push();

Pull latest state from server.

const hasChanges = await useStore.vault.pull();
// => true if server had newer data

Reload state from local storage.

await useStore.vault.rehydrate();

Check if initial hydration is complete.

if (useStore.vault.hasHydrated()) {
// Safe to use store
}

Get current sync status.

const status = useStore.vault.getSyncStatus();
// "idle" | "syncing" | "synced" | "error" | "offline"

Check if offline queue has pending changes.

if (useStore.vault.hasPendingChanges()) {
// Changes waiting to sync
}

Delete all stored data (local and server).

await useStore.vault.clearStorage();

Subscribe to hydration start.

const unsubscribe = useStore.vault.onHydrate((state) => {
console.log("Hydration starting");
});

Subscribe to hydration complete.

const unsubscribe = useStore.vault.onFinishHydration((state) => {
console.log("Hydration complete", state);
});
import type { VaultOptions, SyncStatus } from "@zod-vault/zustand";
type SyncStatus = "idle" | "syncing" | "synced" | "error" | "offline";
interface VaultOptions<S, PersistedState = S> {
name: string;
recoveryKey: string;
server?: string;
getToken?: () => string | null;
partialize?: (state: S) => PersistedState;
merge?: (persisted: unknown, current: S) => S;
skipHydration?: boolean;
syncInterval?: number;
storage?: VaultStorage;
prefix?: string;
onRehydrateStorage?: (state: S) => ((state?: S, error?: unknown) => void) | void;
}