Why Vault?
Native browser storage APIs require manual serialisation, no types, and separate APIs per backend.
ts
// Before — raw localStorage with no typing
const raw = localStorage.getItem('app:users:1');
const user = raw ? JSON.parse(raw) : null; // unknown type, no TTL, no queries
// After — Vault typed adapter
import { createLocalStorage, table } from '@vielzeug/vault';
type User = { id: number; name: string; age: number };
const schema = { users: table<User>('id') };
const db = createLocalStorage({ name: 'app', schema });
const user = await db.get('users', 1); // User | undefined — fully typed
await db.put('users', { id: 2, name: 'Bob', age: 25 }, ttl.hours(1)); // TTL built in
const adults = await db.query('users').between('age', 18, 99).orderBy('name').toArray();| Feature | Vault | Dexie.js | idb-keyval | Raw Web Storage |
|---|---|---|---|---|
| Bundle size | 8.6 KB | ~26 kB | ~1.3 kB | Native |
| TypeScript schema types | ||||
| Query builder | ||||
| TTL | Manual | |||
| Multiple backends | IDB only | IDB only | localStorage only | |
| Reactivity | liveQuery | |||
| Zero dependencies | Native |
Use Vault when you need typed, queryable browser storage with TTL and reactivity across LocalStorage, SessionStorage, IndexedDB, and Memory from a single consistent API.
Consider alternatives when you need a mature IDB-first solution with a large ecosystem — use Dexie.js. For the smallest possible IDB wrapper without abstractions, use idb-keyval. For raw performance without any library, use the Web Storage and IndexedDB APIs directly.
Installation
sh
pnpm add @vielzeug/vaultsh
npm install @vielzeug/vaultsh
yarn add @vielzeug/vaultQuick Start
ts
import { createIndexedDB, table, ttl } from '@vielzeug/vault';
type User = { id: number; name: string; age: number };
const schema = {
users: table<User>('id'),
};
const db = createIndexedDB({ name: 'app', schema, version: 1 });
await db.put('users', { id: 1, name: 'Alice', age: 30 });
await db.put('users', { id: 2, name: 'Bob', age: 25 }, ttl.hours(1));
const adults = await db.query('users').between('age', 18, 99).orderBy('name').toArray();
void adults;Features
table<T>(key)— typed schema entry; infers record type and primary-key field; chain.ttl(ms)for a per-table default TTLcreateLocalStorage/createSessionStorage/createIndexedDB/createMemory— four adapters sharing oneAdapter<S>interface; swap backends without touching application codeput/putAll— write one or many records; TTL enforced via the brandedTtlMstypeget/getAll/getMany— point lookups and bulk fetch; preserves key order, missing keys yieldundefinedupdate(table, key, changes)— shallow-merge partial fields;upsertfor read-modify-write;getOrDefaultfor read-or-insertdelete/deleteMany/clear— single, bulk, or full-table deletionquery(table)— lazyQueryBuilderwith.filter(),.equals(),.between(),.startsWith(),.orderBy(),.limit(),.offset(); terminal.toArray()/.first()/.delete()observe(table, fn)— subscribe to table changes; fires immediately with current snapshot then on every mutation; returns unsubscribe functionobserveMany(tables, fn)— combined snapshot across multiple tables; coalesces batch writes into one callbackwatch(table)—AsyncIterableof fresh snapshots;mode: 'latest'drops intermediates;signalstops from outsidebatch(tables, tx => ...)— deferred observer notifications on all adapters; atomic IDB transaction on IndexedDBttl.ms / .seconds / .minutes / .hours / .days— branded duration helpers; raw numbers are rejected by the type systempruneExpired/scheduleExpiredPrune— sweep expired records manually or on an interval; passsignalto auto-stop on abortkeys(table, filter?)— return primary keys; pass a predicate to filter without loading records into userland firstcreateVersionedCodec— versioned codec for safe upgrades; old records decode with their original codec as long as it is still registerediterate(table)— cursor-basedAsyncIterableover live records (memory adapter only); avoids loading the full table into memory- Ripple signals plugin, Rune logger plugin, and Spell validators plugin — pass any compatible object; structural, not coupled
Documentation
See Also
- Ripple — sync persisted values into signals for reactive UI updates whenever storage changes
- Courier — HTTP client; hydrate Courier's cache from Vault on startup to avoid redundant network requests
- Rune — structured logger; audit storage reads and writes with a Rune transport
- Spell — schema validation; pass a Spell schema to Vault to type-gate values before they are persisted