Persistence helpers
This page covers the persistence-specific helpers, option types, and adapter contracts used by createPersistencePlugin().
Persistence adapter contract
PersistenceAdapter is the shape the plugin expects for storage backends.
getItem(key)reads a stored snapshot and returnsnullwhen missing.setItem(key, value)writes the encoded snapshot.removeItem(key)deletes a stored snapshot.clear?()andkeys?()are optional convenience methods for richer backends.
Built-in adapters
createLocalStorageAdapter(storage?)for browserlocalStorageor a custom storage-like objectcreateSessionStorageAdapter(storage?)for browsersessionStorageor a custom storage-like objectcreateMemoryStorageAdapter()for tests and server-side fallback storagecreateIndexedDbAdapter(database)for async databases that already exposeget,set,delete,clear, andkeys
The browser adapters gracefully handle missing storage and quota errors.
Compression helpers
createLzStringCompression() returns a PersistCompression implementation that prefixes encoded values with lz: and decodes them with lz-string.
Use compression when persisted state is large enough to justify the encoding overhead.
PersistOptions
PersistOptions configures how a store is hydrated and flushed.
Important fields:
adapter— required storage backendversion— required finite version numberkey— optional storage key, defaults to the store idpick— persist only the listed state keysomit— persist every state key except the listed onescompression— optional compressor/decompressorserialize— optional custom serializerdeserialize— optional custom deserializermigrate— optional version migration function used by the built-in deserializer pathonError— callback for failed auto-flush writesdebounce— trailing-edge delay for automatic writesttl— discard persisted state older than the configured age in milliseconds
pick and omit are mutually exclusive. Stately enforces that rule both in the types and at runtime. Providing both at the same time will throw an error at store registration time.
serialize(envelope) and deserialize(raw) are typed against the concrete store state. Interface-shaped stores therefore keep their full field types inside custom persistence hooks instead of degrading to Record<string, unknown>.
Example:
import {
createMemoryStorageAdapter,
createPersistencePlugin,
createStateManager,
defineStore
} from '@selfagency/stately';
const manager = createStateManager().use(createPersistencePlugin());
export const usePreferencesStore = defineStore('preferences', {
state: () => ({ theme: 'dark', compact: false }),
persist: {
adapter: createMemoryStorageAdapter(),
version: 1,
key: 'stately:preferences'
}
});Selective persistence with pick and omit
Use pick when only a few fields should survive reloads:
persist: {
adapter: createMemoryStorageAdapter(),
version: 1,
pick: ['theme', 'compact']
}Use omit when most fields should persist and only a few should stay ephemeral:
persist: {
adapter: createMemoryStorageAdapter(),
version: 1,
omit: ['token']
}Schema upgrades with migrate
Use migrate when the persisted shape changes between versions:
persist: {
adapter: createMemoryStorageAdapter(),
version: 2,
migrate(state, fromVersion) {
if (fromVersion === 1) {
return {
theme: state.theme ?? 'dark',
compact: Boolean(state.compact)
};
}
return {
theme: 'dark',
compact: false
};
}
}migrate only applies when you use the built-in deserializer path. If you provide a fully custom deserialize(), your custom function owns the migration logic too.
TTL expiry
Use ttl when persisted state should expire automatically:
persist: {
adapter: createMemoryStorageAdapter(),
version: 1,
ttl: 60_000
}When ttl is set, Stately wraps the persisted payload in a timestamp envelope. If that timestamp is too old when the store rehydrates, the persisted state is discarded and the store falls back to its initial state.
ttl is evaluated before migrate. If the persisted state has expired, migration is skipped entirely and the store starts from its current initial state.
If you need to preserve TTL behavior across version upgrades, bump version and provide a migrate function for the previous version so that any unexpired payloads are still promoted rather than silently dropped.
Debounced writes
Use debounce when the store mutates frequently and you want to reduce write pressure on the adapter:
persist: {
adapter: createMemoryStorageAdapter(),
version: 1,
debounce: 250
}This is useful for draft editing, drag interactions, or other high-frequency updates.
PersistController
The plugin exposes a controller on each persisted store as $persist.
readyresolves after the initial rehydration attemptflush()writes the current snapshot immediatelyrehydrate()re-reads persisted state on demandclear()removes the stored snapshotpause()andresume()temporarily disable automatic writes
This is useful when you need to batch updates or recover from a storage failure without tearing down the store.
During history replay, persistence writes are intentionally suppressed so you do not overwrite current durable state with historical snapshots.
Public persistence types
The persistence module also exports PersistEnvelope and PersistCompression so consumers can type custom serializers, migrations, or storage bridges without guessing at the shape. PersistEnvelope<State> preserves the concrete store state through custom persistence pipelines.