KeyPlex is a zero-dependency, modular library for managing namespaced key-value storage over the Web Storage API. It provides an abstract class that can be extended to create custom storage backends, plus ready-made implementations for localStorage and sessionStorage.
- Namespace support: automatically scopes keys using a customizable namespace (default derived from the current domain).
- TTL: per-key time-to-live with lazy expiration — no timers.
- Reactivity:
watch()for same-tab and cross-tab changes. - Wildcard deletion: batch-delete by prefix (
prefix%) or suffix (%suffix). - Safe mode: optionally swallow storage errors (quota exceeded, disabled storage) instead of throwing.
- Custom serializers: swap JSON for any format.
- SSR-safe: nothing touches browser globals until first use.
npm install @darcas/keyplexOr, if you're using yarn:
yarn add @darcas/keyplexKeyPlex includes two built-in implementations:
LocalPlex: useslocalStorageas the backend.SessionPlex: usessessionStorageas the backend.
import { LocalPlex, SessionPlex } from '@darcas/keyplex'
const localStore = new LocalPlex('myNamespace')
localStore.set('key', { name: 'KeyPlex' })
const value = localStore.get('key')
console.log(value) // { name: 'KeyPlex' }
if (localStore.has('key')) {
console.log('Key exists!')
}
localStore.del('key')
const sessionStore = new SessionPlex('sessionNamespace')
sessionStore.set('sessionKey', 'Session Data')The constructor accepts either a namespace string or an options object:
const store = new LocalPlex({
dbname: 'myNamespace', // default: reversed hostname (e.g. app.example.com → com.example.app)
safe: true, // storage errors are swallowed instead of thrown
serializer: mySerializer,
})Without arguments, the namespace is derived from location.href. In non-browser environments pass an explicit dbname, otherwise an error is thrown when the namespace is first needed.
Entries can expire. Expiration is checked lazily on read — expired entries are purged and the default is returned:
// Expires in 1 hour
store.set('token', token, { ttl: 3600 })
store.get('token', null) // null after expiry; the entry is removed tooUse % at the end of a key to match all keys with that prefix, or at the beginning to match by suffix:
store.set('user/123/profile', { name: 'John Doe' })
store.set('user/123/settings', { theme: 'dark' })
store.set('user/124/profile', { name: 'Jane Doe' })
// Delete everything under user/123
store.del('user/123%')
// Delete every "profile" key across all users
store.del('%profile')Notes:
%in both positions (%text%) matches keys containingtext.- A bare
%matches every key in the namespace. - Keys are stored internally as
@<dbname>/<key>.
watch(pattern, callback) notifies observers when matching keys change:
const unsubscribe = store.watch('user%', (key, value) => {
console.log(`user key changed: ${key}`, value)
})
unsubscribe() // stop listeningSemantics:
- Same tab: mutations made through other instances sharing the same namespace are delivered. The instance that performed the operation does not notify its own watchers.
- Cross-tab: changes from other tabs are delivered via the native
storageevent. - Deletions deliver
nullas the new value. - Patterns support the same wildcards as
del.
store.keys() // all live keys in this namespace, without the prefix
store.clear() // delete every key in this namespace (only this one)Both exclude and purge expired entries.
To create a custom storage backend, extend KeyPlex and implement four protected methods:
allKeys: return all raw keys in the backend (namespacing is handled by the base class).getItem: retrieve a raw value by its namespaced path.setItem: store a raw value by its namespaced path.removeItem: remove a value by its namespaced path.
import { KeyPlex } from '@darcas/keyplex'
class CustomStorage extends KeyPlex {
private store = new Map<string, string>()
protected allKeys = (): string[] => [ ...this.store.keys() ]
protected getItem = (key: string): string | null => this.store.get(key) ?? null
protected setItem = (key: string, value: string): void => { this.store.set(key, value) }
protected removeItem = (key: string): void => { this.store.delete(key) }
}
const customStore = new CustomStorage('customNamespace')
customStore.set('customKey', 'Custom Value')By default, storage errors propagate. With safe: true they are swallowed:
get/hasreturn the default /falseset/del/clearfail silently
Useful when storage may be unavailable (private browsing, quota limits).
Values are wrapped in an internal envelope before serialization, so any string-based serializer works:
import { LocalPlex, type Serializer } from '@darcas/keyplex'
const base64Serializer: Serializer = {
serialize: (value) => btoa(JSON.stringify(value)),
deserialize: (data) => JSON.parse(atob(data)),
}
const store = new LocalPlex({ dbname: 'ns', serializer: base64Serializer })The Serializer interface is generic (deserialize<T>(data): T, serialize<T>(value: T): string), so typed backends can implement it against a specific shape.
| Method | Description |
|---|---|
get<T>(key) |
Returns the value typed as T | null, or null if missing/expired. |
get<T>(key, def) |
Returns the value typed as T, or def if missing/expired. |
set<T>(key, value, options?) |
Stores the value. options.ttl sets expiry in seconds. |
del(key) |
Deletes a key. Supports % wildcards (prefix/suffix). |
has(key) |
true if the key exists and is not expired. |
keys() |
All live keys in the namespace, prefix stripped. |
clear() |
Deletes all keys in the namespace. |
watch(pattern, callback) |
Observes matching keys. Returns an unsubscribe function. |
dbname |
The resolved namespace (read-only). |
Abstract methods for subclasses: allKeys, getItem, setItem, removeItem.
- Storage format changed: values are now stored in an envelope (
{ t: '@keyplex', v, e? }). Existing v1 entries (raw JSON) remain readable: they are recognized on first access and transparently rewritten in the new format — no migration step is required, and the rewrite never triggerswatchnotifications. Note that{ t: '@keyplex' }is now a reserved shape for top-level values. has()fixed: it no longer returnsfalsefor keys holdingnull.keys()is now public and filtered by namespace; custom backends implement the protectedallKeys()instead.- lodash dependencies removed — the package has zero runtime dependencies.
- Constructor accepts options:
{ dbname, safe, serializer }in addition to the plain string.
If you'd like to contribute to the project, feel free to fork it and create a pull request. Please ensure that your changes are well-tested and properly documented.
This project is licensed under the MIT License. See the LICENSE file for details.
Made with ❤️ by Dario Casertano (DarCas).