API Overview
| Symbol | Purpose | Execution mode | Common gotcha |
|---|---|---|---|
createMemory() | In-memory document store | Async API | Import from /memory; codecs optional |
createLocalStorage() / createSessionStorage() | Web Storage-backed key-value stores | Async API | Codecs required; available only where the corresponding Web API exists |
createIndexedDB() | Browser document store with transactions and cursor iteration | Async API | Import from /indexeddb; codecs required |
createSQLite() | Driver-neutral SQLite document store | Async API over a synchronous driver | Import from /sqlite; codecs required |
table() | Typed record schema | Sync | The key field must be a string or finite number |
validatorCodec() | Create an identity-encoding codec from a parser | Sync | Spell schemas can also be passed directly |
ttl | Valid expiration durations | Sync | Durations must be positive |
Package Entry Points
| Import | Purpose |
|---|---|
@vielzeug/vault | Adapter-free schemas, TTL, errors, codecs, derived helpers, and shared types |
@vielzeug/vault/memory | createMemory |
@vielzeug/vault/local-storage | createLocalStorage |
@vielzeug/vault/session-storage | createSessionStorage |
@vielzeug/vault/indexeddb | createIndexedDB, the migrate upgrade-hook types, and IndexedDB-only types |
@vielzeug/vault/sqlite | createSQLite, the SQLite driver protocol types, and TransactionContext |
Schemas, Codecs, and TTL
table()
function table<T extends object, Key extends keyof T & string = keyof T & string>(
key: Key & (T[Key] extends VaultKey ? unknown : never),
options?: { defaultTtl?: number; indexes?: readonly (keyof T & string)[] },
): SchemaEntry<T, Key>;Defines a typed table and its primary-key field.
| Parameter | Description |
|---|---|
key | A record field whose values are string or finite number keys |
options.defaultTtl | Per-table default TTL in milliseconds |
options.indexes | IndexedDB secondary index fields |
Returns: A SchemaEntry describing the table.
import { table, ttl } from '@vielzeug/vault';
const users = table<{ id: number; email: string }>('id', {
indexes: ['email'],
defaultTtl: ttl.days(7),
});validatorCodec()
function validatorCodec<T>(validator: { parse(value: unknown): T }): RecordCodec<T>;Wraps any object with a parse(value): T method as an identity-encoding RecordCodec. Parser schemas can also be passed directly in codecs; a Spell schema requires no adapter.
Returns: A RecordCodec<T>.
import { s } from '@vielzeug/spell';
const UserSchema = s.object({ id: s.number(), name: s.string() });
const store = createLocalStorage({
name: 'app',
schema: { users: table<{ id: number; name: string }>('id') },
codecs: { users: UserSchema },
});Custom RecordCodec values validate writes by round-tripping decode(encode(value)). IndexedDB codecs must preserve declared indexed field names and values in their encoded object.
ttl
const ttl: {
days(n: number): number;
hours(n: number): number;
minutes(n: number): number;
ms(n: number): number;
seconds(n: number): number;
};Creates a finite, positive duration in milliseconds for writes and table defaults.
Returns: number.
import { ttl } from '@vielzeug/vault';
const cacheLifetime = ttl.minutes(5);Factories
Durable factories (LocalStorage, SessionStorage, IndexedDB, SQLite) require codecs. Memory may omit them.
createMemory()
function createMemory<S extends AnySchema>(options: MemoryStoreOptions<S>): DocumentVaultStore<S>;Creates an in-memory document store with batch() and iterate(). batch() snapshots the declared tables and restores them when the callback throws. Codecs are optional because memory values never cross a trust boundary.
| Parameter | Description |
|---|---|
schema | Tables created by table() |
codecs | Optional per-table codecs |
Returns: DocumentVaultStore<S>.
import { table } from '@vielzeug/vault';
import { createMemory } from '@vielzeug/vault/memory';
const store = createMemory({ schema: { users: table<{ id: number; name: string }>('id') } });createLocalStorage()
function createLocalStorage<S extends AnySchema>(options: DurableStoreOptions<S> & {
name: string;
}): KeyValueVaultStore<S>;Creates a namespaced localStorage store. Codecs are required.
| Parameter | Description |
|---|---|
schema | Tables created by table() |
codecs | Required per-table codecs |
name | Required storage namespace |
A write that exceeds the browser quota rejects with VaultQuotaError.
Returns: KeyValueVaultStore<S>.
import { table } from '@vielzeug/vault';
import { createLocalStorage } from '@vielzeug/vault/local-storage';
const store = createLocalStorage({
name: 'app',
schema: { settings: table<{ id: string }>('id') },
codecs: { settings: SettingsSchema },
});createSessionStorage()
function createSessionStorage<S extends AnySchema>(options: DurableStoreOptions<S> & {
name: string;
}): KeyValueVaultStore<S>;Creates a namespaced sessionStorage store. Its options and return type match createLocalStorage().
Returns: KeyValueVaultStore<S>.
import { table } from '@vielzeug/vault';
import { createSessionStorage } from '@vielzeug/vault/session-storage';
const store = createSessionStorage({
name: 'checkout',
schema: { cart: table<{ id: string }>('id') },
codecs: { cart: CartSchema },
});createIndexedDB()
function createIndexedDB<S extends AnySchema>(options: DurableStoreOptions<S> & {
migrate?: MigrationFn;
name: string;
version?: number;
}): IndexedDbVaultStore<S>;Creates an IndexedDB document store with atomic batches, lazy cursor iteration, and optional schema migrations. Codecs are required.
| Parameter | Description |
|---|---|
schema | Tables and IndexedDB secondary indexes |
codecs | Required per-table codecs |
name | Required database name |
version | Positive schema version; defaults to 1 |
migrate | Synchronous onupgradeneeded callback. Object stores and schema-declared indexes are created automatically; use the hook only for deletes or record transforms |
Returns: IndexedDbVaultStore<S>. Its getAllByIndex(table, field, value) method queries a declared IndexedDB index without scanning the full table.
import { table } from '@vielzeug/vault';
import { createIndexedDB } from '@vielzeug/vault/indexeddb';
const store = createIndexedDB({
name: 'app',
schema: { users: table<{ id: number; role: string }>('id', { indexes: ['role'] }) },
codecs: { users: UserSchema },
});
const admins = await store.getAllByIndex('users', 'role', 'admin');createSQLite()
function createSQLite<S extends AnySchema>(options: SQLiteVaultOptions<S>): DocumentVaultStore<S>;Creates a namespaced SQLite document store with atomic batches and keyset-paginated iteration. Codecs are required. It accepts an application-provided positional-parameter driver and never opens or imports a runtime driver.
| Parameter | Description |
|---|---|
schema | Tables created by table() |
codecs | Required per-table codecs |
database | Caller-provided SQLiteDatabase connection |
name | Namespace within the connection |
closeOnDispose | Closes the connection during disposal; defaults to false |
Returns: DocumentVaultStore<S>.
import { DatabaseSync } from 'node:sqlite';
import { table } from '@vielzeug/vault';
import { createSQLite } from '@vielzeug/vault/sqlite';
const store = createSQLite({
database: new DatabaseSync(':memory:'),
name: 'tests',
schema: { users: table<{ id: number; name: string }>('id') },
codecs: { users: UserSchema },
});Node DatabaseSync, Bun Database, and Deno jsr:@db/sqlite Database satisfy the protocol. Codec output may be any JSON-compatible value. During a batch() callback, calls on every Vault store sharing that connection reject; use tx.* instead.
Store Capabilities
KeyValueVaultStore
interface KeyValueVaultStore<S extends AnySchema> {
clear(table): Promise<void>;
count(table): Promise<number>;
delete(table, key): Promise<boolean>;
deleteMany(table, keys): Promise<number>;
get(table, key): Promise<Record | undefined>;
getAll(table): Promise<Record[]>;
getMany(table, keys): Promise<(Record | undefined)[]>;
has(table, key): Promise<boolean>;
isEmpty(table): Promise<boolean>;
keys(table, filter?): Promise<VaultKey[]>;
update(table, key, changes, ttl?): Promise<Record | undefined>;
upsert(table, key, update, ttl?): Promise<Record>;
observe(table, listener, options?): Unsubscribe;
pruneExpired(): Promise<Record<string, number>>;
put<K extends keyof S & string>(table: K, value: RecordOf<S, K>, ttl?: number): Promise<RecordOf<S, K>>;
putAll(table, values, ttl?): Promise<void>;
readonly disposalSignal: AbortSignal;
dispose(): Promise<void>;
readonly disposed: boolean;
[Symbol.asyncDispose](): Promise<void>;
}The portable key-value store API is returned by Memory and Web Storage factories. put(), update(), and upsert() return the canonical value produced by codec validation. observe() emits the current table snapshot by default and then emits after mutations.
DocumentVaultStore
interface DocumentVaultStore<S extends AnySchema> extends KeyValueVaultStore<S> {
batch<K extends keyof S & string, R>(
tables: readonly K[],
fn: (tx: TransactionContext<S, K>) => Promise<R>,
): Promise<R>;
iterate<K extends keyof S & string>(table: K): AsyncIterable<RecordOf<S, K>>;
}batch() runs a scoped atomic callback. iterate() lazily yields table records — IndexedDB uses a cursor, SQLite uses keyset pagination. Both are provided by createIndexedDB() and createSQLite().
| Parameter | Description |
|---|---|
tables | Tables the transaction may access |
fn | Async callback that uses only the supplied tx context |
Returns: The callback result after commit.
await store.batch(['users'], async (tx) => {
await tx.put('users', { id: 1, name: 'Ada' });
});
for await (const user of store.iterate('users')) console.log(user);Bound Helpers
Convenience operations are bound to every store and transaction context. For filtering, ordering, and pagination, compose over getAll() — it returns a plain array — or use iterate() on document stores and getAllByIndex() on IndexedDB for declared equality indexes.
| Helper | Signature | Description |
|---|---|---|
has() | (table, key) => Promise<boolean> | Check whether a record exists |
count() | (table) => Promise<number> | Count live records |
isEmpty() | (table) => Promise<boolean> | Check whether a table has no live records |
getMany() | (table, keys) => Promise<(T | undefined)[]> | Fetch multiple records by key |
keys() | (table, filter?) => Promise<VaultKey[]> | Fetch live primary keys, optionally filtered by record |
deleteMany() | (table, keys) => Promise<number> | Delete multiple records, return count |
update() | (table, key, changes, ttl?) => Promise<T | undefined> | Partially update an existing record |
upsert() | (table, key, fn, ttl?) => Promise<T> | Read-modify-write with callback |
const exists = await store.has('users', 1);
const total = await store.count('users');
const [a, b] = await store.getMany('users', [1, 2]);
const updated = await store.update('users', 1, { name: 'Alice' });
const result = await store.upsert('users', 99, (existing) => ({ id: 99, name: existing?.name ?? 'Guest' }));
const all = await store.getAll('users');
const active = all.filter((user) => user.status === 'active').sort((a, b) => a.name.localeCompare(b.name)).slice(0, 20);Bound update() and upsert() are atomic on document stores. Their key-value-store forms are non-atomic convenience operations.
Types
type VaultKey = number | string;
type Unsubscribe = () => void;
type Observer<T> = (records: T[]) => void;
type AnySchema = Record<string, {
defaultTtl?: number;
indexes?: readonly string[];
key: string;
}>;
type SchemaEntry<T extends object, Key extends keyof T & string = keyof T & string> = {
defaultTtl?: number;
indexes?: readonly (keyof T & string)[];
key: Key;
};
type RecordOf<S extends AnySchema, K extends keyof S> =
S[K] extends SchemaEntry<infer R, infer _Key> ? R : never;
type KeyOf<S extends AnySchema, K extends keyof S> =
Extract<S[K] extends SchemaEntry<infer R, infer Key> ? R[Key] : never, VaultKey>;type RecordCodec<T> = {
decode(value: unknown): T;
encode(value: T): unknown;
};
type RecordParser<T> = { parse(value: unknown): T };
type CodecInput<T> = RecordCodec<T> | RecordParser<T>;
type TableCodecs<S extends AnySchema> = {
[K in keyof S]: CodecInput<RecordOf<S, K>>;
};
type MemoryStoreOptions<S extends AnySchema> = {
schema: S;
codecs?: TableCodecs<S>;
};
type DurableStoreOptions<S extends AnySchema> = {
schema: S;
codecs: TableCodecs<S>;
};type MigrationContext = {
db: IDBDatabase;
newVersion: number | null;
oldVersion: number;
tx: IDBTransaction;
};
type MigrationFn = (ctx: MigrationContext) => void;Import MigrationContext and MigrationFn from @vielzeug/vault/indexeddb.
type SQLiteParameter = null | number | string;
interface SQLiteStatement {
all(...parameters: SQLiteParameter[]): readonly Record<string, unknown>[];
finalize?(): void;
get(...parameters: SQLiteParameter[]): Record<string, unknown> | null | undefined;
run(...parameters: SQLiteParameter[]): unknown;
}
interface SQLiteDatabase {
close?(): void;
exec(sql: string): void;
prepare(sql: string): SQLiteStatement;
}
type SQLiteVaultOptions<S extends AnySchema> = DurableStoreOptions<S> & {
closeOnDispose?: boolean;
database: SQLiteDatabase;
name: string;
};interface TransactionContext<S extends AnySchema, K extends keyof S & string = keyof S & string> {
clear<T extends K>(table: T): Promise<void>;
delete<T extends K>(table: T, key: KeyOf<S, T>): Promise<boolean>;
get<T extends K>(table: T, key: KeyOf<S, T>): Promise<RecordOf<S, T> | undefined>;
getAll<T extends K>(table: T): Promise<RecordOf<S, T>[]>;
iterate<T extends K>(table: T): AsyncIterable<RecordOf<S, T>>;
put<T extends K>(table: T, value: RecordOf<S, T>, ttl?: number): Promise<RecordOf<S, T>>;
putAll<T extends K>(table: T, values: RecordOf<S, T>[], ttl?: number): Promise<void>;
}TransactionContext has core CRUD, iterate(), and TTL methods, narrowed to the tables declared in batch(). Import it from @vielzeug/vault/indexeddb or @vielzeug/vault/sqlite.
Errors
| Error | Trigger |
|---|---|
VaultError | Any Vault-originated validation, serialization, storage, or codec error |
VaultDisposedError | An operation after the store or observer hub is disposed |
VaultScopeError | A batch() callback accesses a table outside its declared scope |
VaultQuotaError | A LocalStorage or SessionStorage write exceeds the browser quota |
VaultMigrationError | An IndexedDB migration callback throws |
Every listed error extends VaultError.