@darcas/keyplex · v2.0.4 · MIT

Namespaced storage
that thinks ahead.

Key-value storage over localStorage and sessionStorage — with built-in TTL, wildcard deletion and cross-tab watching. Fully typed, zero dependencies.

npm ↗
app.example.com:user.theme
quickstart.ts
import { LocalPlex } from '@darcas/keyplex'

const store = new LocalPlex()
// namespace auto-derived: app.example.com → com.example.app

store.set('user.theme', 'dark')
store.set('session.token', token, { ttl: 900 })

store.get<string>('user.theme') // 'dark'
store.keys()                    // ['user.theme', 'session.token']

Contents: features

Everything localStorage should have shipped with.

Reasons to stop using raw Web Storage in 2026 — each one maps to a real line of code you don’t have to write anymore.

Built-in TTL

Set it and forget it.

Pass { ttl } in seconds when you write. Expired entries are purged lazily on read — no timers, no sweeps, no garbage.

Wildcard deletion

Delete by pattern, not by name.

Trailing % matches prefixes, leading % suffixes, both sides contains. store.del('cache:%') clears a whole family in one call.

Cross-tab watching

Every tab, always in sync.

watch(pattern, cb) delivers changes from any tab via the native storage event. No polling, no BroadcastChannel plumbing.

Automatic namespaces

One browser. Many apps. No collisions.

Keys are scoped under a namespace derived from your hostname — reversed, like app.example.com → com.example.app. Or pass your own.

Safe mode

Storage fails? Your app doesn’t.

With safe: true, quota errors and disabled storage are swallowed instead of thrown — Safari private mode included.

Pluggable serializers

Bring your own format.

JSON by default. Implement the two-method Serializer interface to store MessagePack, encrypted payloads, anything string-shaped.

Extend to anything

Four methods to rule a backend.

KeyPlex is an abstract class: implement getItem/setItem/removeItem/allKeys and you have the full API over IndexedDB, memory or the network.

SSR-friendly

Renders anywhere.

The default namespace resolves lazily from location.hostname. Pass an explicit dbname and it runs on the server without a browser.

Zero dependencies

~3 KB min+gzip.

A single source file compiled to ESM with bundled TypeScript types. Nothing to audit, nothing to update.

ttl.ts
// Expires after 15 minutes — purged lazily on read.
// No timers, no sweeps.
store.set('session.token', token, { ttl: 15 * 60 })

store.has('session.token')  // true
// …15 minutes later…
store.get('session.token')  // null (and the entry is gone)
wildcards-and-watch.ts
store.del('cache:%')        // prefix match
store.del('%:draft')        // suffix match
store.del('%avatar%')       // contains

// Live updates from any tab — backed by the native
// storage event, no polling:
const stop = store.watch('user:%', (key, value) => {
    render(key, value)
})

stop() // unsubscribe

Contents: live demo

This page runs on it. Try the real thing.

No screenshots. The commands below call the actual @darcas/keyplex package installed on this very page — your browser, your storage.

Commands

// Web Storage is unavailable in this context (safe mode keeps everything running)

Live state · keys()

  • // namespace is empty

Watch log · watch('%')

  • // no events yet — open this page in a second tab and change a // value there

By design, a KeyPlex instance never notifies its own watchers — same-tab events reach sibling instances of the same namespace, cross-tab events arrive via the native storage listener.

Contents: API surface

Nine methods. That’s the whole surface.

Fully typed out of the box — the package ships its own declarations. No configuration object larger than the library itself.

MethodReturnsNotes
new LocalPlex(options?)LocalPlexoptions: string (namespace) or { dbname?, safe?, serializer? }. SessionPlex is identical over sessionStorage.
get<T>(key)T | nullReads, purges expired entries lazily, migrates legacy values on the fly. Overload get(key, def) returns your fallback.
set(key, value, options?)voidWraps value in the envelope; options.ttl (seconds) stamps the expiry epoch.
del(pattern)voidExact delete — or wildcard: trailing % prefix-match, leading % suffix-match, both contains.
has(key)booleanExistence check with lazy expiry purge. Never triggers a legacy rewrite.
keys()string[]All keys in the namespace, relative names only. Expired entries are dropped as a side effect.
clear()voidRemoves every key in the namespace and emits watch events for each.
watch(pattern, callback)() => voidSubscribes to changes matching pattern; returns the unsubscribe function. Cross-tab via storage events, same-tab across sibling instances.
memory-backend.ts
import { KeyPlex } from '@darcas/keyplex'

class MemoryPlex extends KeyPlex {
    private map = new Map<string, string>()

    protected allKeys = (): string[] => [...this.map.keys()]
    protected getItem = (key: string): string | null =>
        this.map.get(key) ?? null
    protected setItem = (key: string, value: string): void => {
        this.map.set(key, value)
    }
    protected removeItem = (key: string): void => {
        this.map.delete(key)
    }
}

// Full API — TTL, wildcards, watching — over any backend.

Contents: side by side

Raw localStorage vs KeyPlex.

The same eight concerns you’d eventually solve by hand — solved once, typed, and shipped in ~3 KB.

ConcernRaw localStorageKeyPlex
TTL / expiry✗ DIY timestamps on every write✓ { ttl } option, lazy purge on read
Namespacing✗ Manual prefixes, hope for the best✓ Automatic reversed-hostname namespace
Bulk deletion✗ Loop over localStorage yourself✓ del('cache:%') with % wildcards
Cross-tab sync✗ Hand-rolled storage listeners per feature✓ watch('user:%', cb), one line
Type safety✗ Strings in, strings out✓ Generics + shipped .d.ts
Storage failures✗ Throws in private mode / on quota✓ safe: true swallows gracefully
Custom formats✗ Parse/stringify everywhere✓ Serializer interface, set once
Custom backends✗ Rewrite everything✓ Extend KeyPlex, four methods

Contents: get started

One install. Zero config.

Drop it in, give it a namespace (or don’t), and start storing things that expire, match patterns and sync across tabs.