Skip to content

API Overview

SymbolPurposeExecution modeCommon gotcha
createMemory()In-memory portable storeAsync APIImport from /memory
createLocalStorage() / createSessionStorage()Web Storage-backed portable storesAsync APIAvailable only where the corresponding Web API exists
createIndexedDB()Browser transactions and cursor iterationAsync APIImport from /indexeddb
createSQLite()Driver-neutral SQLite storeAsync API over a synchronous driverImport from /sqlite
table()Typed record schemaSyncThe key field must be a string or finite number
ttlValid expiration durationsSyncDurations must be positive
scheduleExpiredPrune()Periodic TTL cleanupSync setup, async workStop it or give it an abort signal

Package Entry Points

ImportPurpose
@vielzeug/vaultAdapter-free schemas, TTL, errors, pruning, queries, and shared types
@vielzeug/vault/memorycreateMemory
@vielzeug/vault/local-storagecreateLocalStorage
@vielzeug/vault/session-storagecreateSessionStorage
@vielzeug/vault/indexeddbcreateIndexedDB, migrations, and IndexedDB-only types
@vielzeug/vault/sqlitecreateSQLite, the SQLite driver protocol types, and TransactionContext

Schemas and TTL

table()

ts
function table<T extends object, Key extends keyof T & string = keyof T & string>(
  key: Key & (T[Key] extends VaultKey ? unknown : never),
): TableBuilder<T, Key>;

Defines a typed table and its primary-key field.

ParameterDescription
keyA record field whose values are string or finite number keys

Returns: A table builder. Call .ttl() to set a default expiry and .index() to declare an IndexedDB secondary index.

ts
import { table, ttl } from '@vielzeug/vault';

const users = table<{ id: number; email: string }>('id').index('email').ttl(ttl.days(7));

ttl

ts
const ttl: {
  days(n: number): TtlMs;
  hours(n: number): TtlMs;
  minutes(n: number): TtlMs;
  ms(n: number): TtlMs;
  seconds(n: number): TtlMs;
};

Creates a branded, finite, positive duration for writes and table defaults.

Returns: TtlMs.

ts
import { ttl } from '@vielzeug/vault';

const cacheLifetime = ttl.minutes(5);

isExpired()

ts
function isExpired(expiresAt: number | undefined): boolean;

Reports whether an expiration timestamp has passed.

Returns: true when expiresAt is defined and no later than the current time.

ts
import { isExpired } from '@vielzeug/vault';

if (isExpired(record.expiresAt)) console.log('expired');

Factories

All factory options accept schema, plus optional validators, logger, and onMetrics. The root entry does not export any factory.

createMemory()

ts
function createMemory<S extends AnySchema>(options: {
  name?: string;
  schema: S;
} & BaseAdapterOptions<S>): VaultStore<S>;

Creates an in-memory portable store. A name enables same-origin BroadcastChannel observation between memory stores when the platform provides it.

ParameterDescription
schemaTables created by table()
nameOptional shared memory-store namespace

Returns: VaultStore<S>.

ts
import { table } from '@vielzeug/vault';
import { createMemory } from '@vielzeug/vault/memory';

const store = createMemory({ schema: { users: table<{ id: number; name: string }>('id') } });

createLocalStorage()

ts
function createLocalStorage<S extends AnySchema>(options: {
  name: string;
  onQuotaExceeded?: (table: keyof S, error: VaultQuotaError) => 'ignore' | 'throw';
  schema: S;
} & BaseAdapterOptions<S>): VaultStore<S>;

Creates a namespaced localStorage store.

ParameterDescription
nameRequired storage namespace
onQuotaExceededHandles a Web Storage quota error; returning 'ignore' drops that write
schemaTables created by table()

Returns: VaultStore<S>.

ts
import { table } from '@vielzeug/vault';
import { createLocalStorage } from '@vielzeug/vault/local-storage';

const store = createLocalStorage({ name: 'app', schema: { settings: table<{ id: string }>('id') } });

createSessionStorage()

ts
function createSessionStorage<S extends AnySchema>(options: {
  name: string;
  onQuotaExceeded?: (table: keyof S, error: VaultQuotaError) => 'ignore' | 'throw';
  schema: S;
} & BaseAdapterOptions<S>): VaultStore<S>;

Creates a namespaced sessionStorage store. Its options and return type match createLocalStorage().

Returns: VaultStore<S>.

ts
import { table } from '@vielzeug/vault';
import { createSessionStorage } from '@vielzeug/vault/session-storage';

const store = createSessionStorage({ name: 'checkout', schema: { cart: table<{ id: string }>('id') } });

createIndexedDB()

ts
function createIndexedDB<S extends AnySchema>(options: {
  migrate?: MigrationFn;
  name: string;
  schema: S;
  version?: number;
} & BaseAdapterOptions<S>): IndexedDbVaultStore<S>;

Creates an IndexedDB store with atomic batches, lazy cursor iteration, and optional schema migrations.

ParameterDescription
nameRequired database name
schemaTables and IndexedDB secondary indexes
versionPositive schema version; defaults to 1
migrateSynchronous upgrade callback for version changes

Returns: IndexedDbVaultStore<S>.

ts
import { table } from '@vielzeug/vault';
import { createIndexedDB } from '@vielzeug/vault/indexeddb';

const store = createIndexedDB({ name: 'app', schema: { users: table<{ id: number }>('id') } });

createSQLite()

ts
function createSQLite<S extends AnySchema>(options: SQLiteVaultOptions<S>): SQLiteVaultStore<S>;

Creates a namespaced SQLite store with atomic batches and keyset-paginated iteration. It accepts an application-provided positional-parameter driver and never opens or imports a runtime driver.

ParameterDescription
databaseCaller-provided SQLiteDatabase connection
nameNamespace within the connection
schema, validators, logger, onMetricsShared factory options
closeOnDisposeCloses the connection during disposal; defaults to false

Returns: SQLiteVaultStore<S>.

ts
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') },
});

Node DatabaseSync, Bun Database, and Deno jsr:@db/sqlite Database satisfy the protocol. Values must be JSON-compatible plain objects. During a batch() callback, calls on every Vault store sharing that connection reject; use tx.* instead.

Store Capabilities

VaultStore

ts
interface VaultStore<S extends AnySchema> {
  clear<K extends keyof S & string>(table: K): Promise<void>;
  count<K extends keyof S & string>(table: K): Promise<number>;
  delete<K extends keyof S & string>(table: K, key: KeyOf<S, K>): Promise<boolean>;
  deleteMany<K extends keyof S & string>(table: K, keys: KeyOf<S, K>[]): Promise<number>;
  entries<K extends keyof S & string>(table: K): Promise<Array<[KeyOf<S, K>, RecordOf<S, K>]>>;
  get<K extends keyof S & string>(table: K, key: KeyOf<S, K>): Promise<RecordOf<S, K> | undefined>;
  getAll<K extends keyof S & string>(table: K): Promise<RecordOf<S, K>[]>;
  getMany<K extends keyof S & string>(table: K, keys: KeyOf<S, K>[]): Promise<Array<RecordOf<S, K> | undefined>>;
  getOrDefault<K extends keyof S & string>(table: K, key: KeyOf<S, K>, defaultFn: () => RecordOf<S, K>, ttl?: TtlMs): Promise<RecordOf<S, K>>;
  has<K extends keyof S & string>(table: K, key: KeyOf<S, K>): Promise<boolean>;
  isEmpty<K extends keyof S & string>(table: K): Promise<boolean>;
  keys<K extends keyof S & string>(table: K, filter?: (record: RecordOf<S, K>) => boolean): Promise<KeyOf<S, K>[]>;
  put<K extends keyof S & string>(table: K, value: RecordOf<S, K>, ttl?: TtlMs): Promise<void>;
  putAll<K extends keyof S & string>(table: K, values: RecordOf<S, K>[], ttl?: TtlMs): Promise<void>;
  query<K extends keyof S & string>(table: K): QueryBuilder<RecordOf<S, K>>;
  update<K extends keyof S & string>(table: K, key: KeyOf<S, K>, changes: Partial<RecordOf<S, K>>, ttl?: TtlMs): Promise<RecordOf<S, K> | undefined>;
  upsert<K extends keyof S & string>(table: K, key: KeyOf<S, K>, fn: (existing: RecordOf<S, K> | undefined) => RecordOf<S, K>, ttl?: TtlMs): Promise<RecordOf<S, K>>;
  pruneExpired(): Promise<Record<keyof S & string, number>>;
  debug(): Promise<DebugInfo<S>>;
  observe<K extends keyof S & string>(table: K, listener: Observer<RecordOf<S, K>>, options?: { immediate?: boolean; signal?: AbortSignal }): Unsubscribe;
  dispose(): Promise<void>;
  readonly disposed: boolean;
  readonly disposalSignal: AbortSignal;
  [Symbol.asyncDispose](): Promise<void>;
}

The portable store API is returned by every factory. observe() emits the current table snapshot by default and then emits after mutations.


batch()

ts
interface TransactionalVaultStore<S extends AnySchema> extends VaultStore<S> {
  batch<K extends keyof S & string, R>(
    tables: readonly K[],
    fn: (tx: TransactionContext<S, K>) => Promise<R>,
  ): Promise<R>;
}

Runs a scoped atomic callback. IndexedDbVaultStore and SQLiteVaultStore provide it.

ParameterDescription
tablesTables the transaction may access
fnAsync callback that uses only the supplied tx context

Returns: The callback result after commit.

ts
await store.batch(['users'], async (tx) => {
  await tx.put('users', { id: 1, name: 'Ada' });
});

iterate()

ts
interface IterableVaultStore<S extends AnySchema> extends VaultStore<S> {
  iterate<K extends keyof S & string>(table: K): AsyncIterable<RecordOf<S, K>>;
}

Lazily yields table records. IndexedDbVaultStore uses a cursor; SQLiteVaultStore uses keyset pagination.

Returns: An AsyncIterable of records.

ts
for await (const user of store.iterate('users')) console.log(user);

Queries, Pruning, and Migrations

QueryBuilder

ts
interface QueryBuilder<T extends object, N extends T = T> {
  between(field: string, lower: number | string, upper: number | string): QueryBuilder<T, N>;
  count(): Promise<number>;
  delete(): Promise<number>;
  equals<K extends keyof T & string, V extends T[K]>(field: K, value: V): QueryBuilder<T & Record<K, V>>;
  exists(): Promise<boolean>;
  filter(fn: (value: N, index: number, array: N[]) => boolean): QueryBuilder<T, N>;
  first(): Promise<N | undefined>;
  limit(n: number): QueryBuilder<T, N>;
  offset(n: number): QueryBuilder<T, N>;
  orderBy<K extends keyof T>(field: K, direction?: 'asc' | 'desc'): QueryBuilder<T, N>;
  startsWith(field: keyof T, prefix: string, options?: { ignoreCase?: boolean }): QueryBuilder<T, N>;
  toArray(): Promise<N[]>;
  totalCount(): Promise<number>;
}

Builds a lazy table query. count() respects limit() and offset(); totalCount() ignores pagination and ordering.

ts
const page = await store.query('users').startsWith('name', 'A').orderBy('name').limit(20).toArray();

scheduleExpiredPrune()

ts
function scheduleExpiredPrune<S extends AnySchema>(
  adapter: Pick<VaultStore<S>, 'pruneExpired'>,
  options: {
    interval: number;
    onError?: (error: unknown) => void;
    signal?: AbortSignal;
  },
): () => void;

Schedules pruneExpired() at a finite, positive interval.

Returns: A stop function.

ts
import { scheduleExpiredPrune, ttl } from '@vielzeug/vault';

const stop = scheduleExpiredPrune(store, { interval: ttl.hours(1), signal: store.disposalSignal });
stop();

defineMigration()

ts
function defineMigration(steps: MigrationStep[]): MigrationFn;

Builds an idempotent IndexedDB migration callback from schema-change steps.

Returns: An IndexedDB MigrationFn.

ts
import { defineMigration } from '@vielzeug/vault/indexeddb';

const migrate = defineMigration([{ field: 'email', table: 'users', type: 'addIndex' }]);

Types

ts
type VaultKey = number | string;
type TtlMs = number & { readonly [ttlMsBrand]: never };
type Unsubscribe = () => void;
type Observer<T> = (records: T[]) => void;
type AnySchema = Record<string, {
  defaultTtl?: TtlMs;
  indexes?: readonly string[];
  key: string;
}>;
type SchemaEntry<T extends object, Key extends keyof T & string = keyof T & string> =
  T[Key] extends VaultKey ? {
    defaultTtl?: TtlMs;
    indexes?: readonly (keyof T & string)[];
    key: Key;
  } : never;
type TableBuilder<T extends object, Key extends keyof T & string = keyof T & string> =
  SchemaEntry<T, Key> & {
    index: <F extends keyof T & string>(field: F) => TableBuilder<T, Key>;
    ttl: (ms: TtlMs) => TableBuilder<T, 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>;
ts
type BaseAdapterOptions<S extends AnySchema> = {
  logger?: VaultLogger;
  onMetrics?: (event: MetricsEvent) => void;
  schema: S;
  validators?: TableValidators<S>;
};

interface VaultLogger {
  error(messageOrContext?: Record<string, unknown> | Error | string, message?: string): void;
}

interface RecordValidator<T> {
  parse(value: unknown): T;
}

type TableValidators<S extends AnySchema> = {
  [K in keyof S]?: RecordValidator<RecordOf<S, K>>;
};

type MetricsEvent = {
  duration: number;
  operation: 'batch' | 'clear' | 'count' | 'delete' | 'deleteMany' | 'entries' | 'get' | 'getAll' |
    'getMany' | 'getOrDefault' | 'has' | 'isEmpty' | 'keys' | 'put' | 'putAll' | 'query' |
    'queryDelete' | 'update' | 'upsert';
  table: string;
};

type DebugStats = { expiredCount: number; recordCount: number };
type DebugInfo<S extends AnySchema> = { tables: Array<{ name: keyof S & string } & DebugStats> };
ts
type MigrationContext = {
  db: IDBDatabase;
  newVersion: number | null;
  oldVersion: number;
  tx: IDBTransaction;
};

type MigrationFn = (ctx: MigrationContext) => void;

type MigrationStep =
  | { field: string; table: string; type: 'addIndex' }
  | { field: string; table: string; type: 'removeIndex' }
  | { name: string; type: 'addTable' }
  | { name: string; type: 'removeTable' };

interface IndexedDbVaultStore<S extends AnySchema>
  extends TransactionalVaultStore<S>, IterableVaultStore<S> {}
ts
type SQLiteParameter = null | number | string;

interface SQLiteStatement {
  all(...parameters: SQLiteParameter[]): readonly Record<string, unknown>[];
  finalize?(): void;
  get(...parameters: SQLiteParameter[]): Record<string, unknown> | undefined;
  run(...parameters: SQLiteParameter[]): unknown;
}

interface SQLiteDatabase {
  close?(): void;
  exec(sql: string): void;
  prepare(sql: string): SQLiteStatement;
}

type SQLiteVaultOptions<S extends AnySchema> = BaseAdapterOptions<S> & {
  closeOnDispose?: boolean;
  database: SQLiteDatabase;
  name: string;
};

interface SQLiteVaultStore<S extends AnySchema>
  extends TransactionalVaultStore<S>, IterableVaultStore<S> {}

TransactionContext has the same CRUD, query, and TTL methods as VaultStore, narrowed to the tables declared in batch(). Import it from @vielzeug/vault/indexeddb or @vielzeug/vault/sqlite.

Errors

ErrorTrigger
VaultErrorAny Vault-originated validation, serialization, storage, or query error
VaultDisposedErrorAn operation after the store or observer hub is disposed
VaultScopeErrorAn IndexedDB transaction accesses a table outside its declared batch scope
VaultQuotaErrorA LocalStorage or SessionStorage write exceeds the browser quota
VaultMigrationErrorAn IndexedDB migration callback throws

Every listed error extends VaultError.