@absoluteui/services/storage
Typed local, session, memory, and IndexedDB stores with TTL, migrations, cross-tab sync, cookies, and encryption at rest.
import { ... } from '@absoluteui/services/storage';How to use
Section titled “How to use”Typed, namespaced key-value stores over localStorage, sessionStorage, IndexedDB, or memory, with the same async API for all of them. Reach for createStore when you persist user preferences, drafts, or cached lookups; it adds expiry, schema versions, validation, and change events, and falls back to memory during SSR or when storage is blocked. Cookies and a promise wrapper for IndexedDB live here too.
How do I keep typed preferences that expire?
Section titled “How do I keep typed preferences that expire?”Give the store a namespace, a shape check, and a default time to live. Reads never throw: missing, expired, corrupt, or invalid entries read as undefined.
import { createStore } from '@absoluteui/services/storage';
interface GridPrefs { density: 'compact' | 'comfortable'; pageSize: number }const isGridPrefs = (v: unknown): v is GridPrefs => typeof v === 'object' && v !== null && 'density' in v && 'pageSize' in v && typeof v.pageSize === 'number';
let clock = Date.parse('2026-10-05T09:00:00Z');const prefs = createStore<GridPrefs>({ namespace: 'hr-grid', validate: isGridPrefs, ttlMs: 30 * 86_400_000, now: () => clock });await prefs.set('employees', { density: 'compact', pageSize: 50 });await prefs.get('employees'); // => { density: 'compact', pageSize: 50 }await prefs.set('leave', { density: 'comfortable', pageSize: 25 }, { ttlMs: 60_000 }); // per-entry overrideclock += 120_000;await prefs.get('leave'); // => undefinedawait prefs.keys(); // => ['employees']The injected now is only for the example; omit it in applications. prune() removes expired entries in bulk, for example on start-up.
How do I change the shape of stored data without losing it?
Section titled “How do I change the shape of stored data without losing it?”Raise version and pass migrate. Older entries are upgraded on first read and written back; without migrate, they are dropped.
import { createStore } from '@absoluteui/services/storage';
const v1 = createStore<{ compact: boolean }>({ namespace: 'hr-layout' });await v1.clear();await v1.set('timesheet', { compact: true });
const v2 = createStore<{ density: 'compact' | 'comfortable' }>({ namespace: 'hr-layout', version: 2, migrate: (old, fromVersion) => { const compact = fromVersion === 1 && typeof old === 'object' && old !== null && 'compact' in old && old.compact === true; return { density: compact ? 'compact' : 'comfortable' }; },});await v2.get('timesheet'); // => { density: 'compact' }How do I react when another tab changes a value?
Section titled “How do I react when another tab changes a value?”subscribe reports changes from this store instance (source: 'local') and from other tabs ('remote', through storage events or a BroadcastChannel for IndexedDB). Read the new value with get; pass null to hear about every key.
import { createStore, type StoreChange } from '@absoluteui/services/storage';
const settings = createStore<string>({ namespace: 'hr-settings' });const seen: StoreChange[] = [];const stop = settings.subscribe('theme', (change) => { seen.push(change); });await settings.set('theme', 'dark');seen; // => [{ key: 'theme', kind: 'set', source: 'local' }]stop();settings.close();How do I encrypt what I store?
Section titled “How do I encrypt what I store?”Wrap a string store with createEncryptedStore and an AES-GCM key. Derive the key from a passphrase with deriveKeyPbkdf2 and keep the salt (it is not secret) next to the data.
import { deriveKeyPbkdf2, randomSalt } from '@absoluteui/services/crypto';import { createEncryptedStore, createStore } from '@absoluteui/services/storage';
interface Draft { employeeId: number; note: string }const key = await deriveKeyPbkdf2('passphrase entered by the user', randomSalt());const raw = createStore<string>({ namespace: 'hr-drafts', backend: 'indexeddb' });const drafts = createEncryptedStore<Draft>(raw, key);await drafts.set('review-42', { employeeId: 42, note: 'Promotion case' });await drafts.get('review-42'); // => { employeeId: 42, note: 'Promotion case' }(await raw.get('review-42'))?.includes('Promotion'); // => falseThis protects data at rest (disk, backups). Script running in the page can use the same key, so it does not protect against XSS.
How do I read and write cookies?
Section titled “How do I read and write cookies?”Use cookies only for small, non-sensitive values such as a locale or consent state. setCookie defaults to Secure, SameSite=Lax, and Path=/, URI-encodes the value, and refuses cookies over MAX_COOKIE_BYTES (SVC_COOKIE_TOO_LARGE).
import { getCookie, removeCookie, setCookie } from '@absoluteui/services/storage';
setCookie('hr-locale', 'en-GB', { maxAge: 365 * 86_400 });getCookie('hr-locale'); // e.g. 'en-GB'removeCookie('hr-locale');JavaScript cannot read or set HttpOnly cookies; set session cookies from the server.
What goes wrong?
Section titled “What goes wrong?”- Bad keys, namespaces, or TTLs throw at once (
SVC_STORAGE_INVALID_KEY,SVC_STORAGE_INVALID_NAMESPACE,SVC_STORAGE_INVALID_TTL); a value that failsvalidatethrowsSVC_STORAGE_INVALID; a full disk rejects withSVC_STORAGE_QUOTA. - In private browsing or during SSR the store silently uses memory: check
persistentafterreadyif losing data matters. - Never keep auth tokens in web storage; any script in the page can read them. Use
HttpOnlycookies set by the server.
import { createStore } from '@absoluteui/services/storage';
const store = createStore<number>({ namespace: 'hr-counters', validate: (v): v is number => typeof v === 'number' });createStore<number>({ namespace: 'hr:counters' }); // => throws SVC_STORAGE_INVALID_NAMESPACEawait store.set('', 1); // => throws SVC_STORAGE_INVALID_KEYawait store.set('visits', Number.NaN, { ttlMs: -1 }); // => throws SVC_STORAGE_INVALID_TTLAPI reference
Section titled “API reference”Cookies
Section titled “Cookies”Functions
Section titled “Functions”getCookie
One cookie’s decoded value, or undefined.
function getCookie(name: string): string | undefined;Example:
const consent = getCookie('consent');getCookies
All readable (non-HttpOnly) cookies as a null-prototype record; {} without a document (SSR).
The first occurrence of a duplicate name wins (the most specific path, per browsers’ ordering).
function getCookies(): Record<string, string>;Example:
getCookies().themeremoveCookie
Deletes a cookie. path and domain must match the values it was set with.
function removeCookie(name: string, options?: Pick<CookieOptions, 'path' | 'domain' | 'secure' | 'sameSite' | 'partitioned'>): boolean;Example:
removeCookie('theme');setCookie
Writes a cookie (value is URI-encoded). Defaults: Path=/, Secure, SameSite=Lax. __Host- and
__Secure- prefixes are validated. HttpOnly cannot be set from JavaScript. Returns false without a document (SSR).
function setCookie(name: string, value: string, options?: CookieOptions): boolean;Example:
setCookie('theme', 'dark', { maxAge: 60 * 60 * 24 * 365 });Constants
Section titled “Constants”MAX_COOKIE_BYTES
Browsers cap one cookie (name + value) at about 4096 bytes.
const MAX_COOKIE_BYTES = 4096;Interfaces
Section titled “Interfaces”CookieOptions
Options for {@link setCookie}.
interface CookieOptions { readonly maxAge?: number; readonly expires?: Date; readonly path?: string; readonly domain?: string; readonly secure?: boolean; readonly sameSite?: 'Strict' | 'Lax' | 'None'; readonly partitioned?: boolean;}Encrypted
Section titled “Encrypted”Functions
Section titled “Functions”createEncryptedStore
Wraps a string store so values are encrypted with AES-GCM (fresh random 96-bit IV per write; AAD binds each
ciphertext to namespace and key, so entries cannot be swapped between keys). TTL, versions and change events
come from the inner store (its migrate sees ciphertext, so version the plaintext yourself if needed).
Protects data at rest only (disk, backups, other OS users). It does NOT protect against XSS: script running in the page can use the same key. Never store auth tokens in web storage; use HttpOnly cookies.
function createEncryptedStore<T>(store: Store<string>, key: CryptoKey, options?: EncryptedStoreOptions<T>): Store<T>;Example:
import { deriveKeyPbkdf2, randomSalt } from '@absoluteui/services/crypto';type Draft = { text: string };const key = await deriveKeyPbkdf2('user passphrase', randomSalt());const drafts = createEncryptedStore<Draft>(createStore<string>({ namespace: 'drafts', backend: 'indexeddb' }), key);await drafts.set('d1', { text: 'Hi' });Interfaces
Section titled “Interfaces”EncryptedStoreOptions
Options for {@link createEncryptedStore}.
interface EncryptedStoreOptions<T> { readonly validate?: (value: unknown) => value is T; readonly onError?: (error: HelperError, key: string) => void;}Indexeddb
Section titled “Indexeddb”Functions
Section titled “Functions”idbError
Maps a DOMException from IndexedDB to a HelperError (quota -> SVC_STORAGE_QUOTA) without leaking platform messages.
function idbError(e: unknown): HelperError;Example:
declare const req: IDBRequest<number>;declare const reject: (error: Error) => void;req.onerror = () => reject(idbError(req.error));idbRequest
Wraps an IDBRequest in a promise.
function idbRequest<T>(request: IDBRequest<T>): Promise<T>;Example:
declare const store: IDBObjectStore;const n = await idbRequest(store.count());openIndexedDb
Opens (and creates or upgrades) an IndexedDB database with a promise API. Handles blocked,
versionchange (closes so other tabs can upgrade) and transaction completion correctly.
Rejects with SVC_IDB_UNAVAILABLE when IndexedDB is absent (SSR, some private modes).
function openIndexedDb(name: string, options?: OpenIndexedDbOptions): Promise<IndexedDbConnection>;Example:
const conn = await openIndexedDb('app', { version: 2, upgrade(db, old) { if (old < 1) db.createObjectStore('drafts'); if (old < 2) db.createObjectStore('files'); },});await conn.put('drafts', { text: 'Hi' }, 'd1');Interfaces
Section titled “Interfaces”IndexedDbConnection
Promise-based IndexedDB connection returned by {@link openIndexedDb}.
interface IndexedDbConnection { readonly db: IDBDatabase; get<T = unknown>(store: string, key: IDBValidKey): Promise<T | undefined>; put(store: string, value: unknown, key?: IDBValidKey): Promise<IDBValidKey>; delete(store: string, key: IDBValidKey | IDBKeyRange): Promise<void>; clear(store: string): Promise<void>; keys(store: string, query?: IDBKeyRange): Promise<IDBValidKey[]>; getAll<T = unknown>(store: string, query?: IDBKeyRange): Promise<T[]>; count(store: string, query?: IDBKeyRange): Promise<number>; transaction<R>(stores: string | readonly string[], mode: IDBTransactionMode, work: (tx: IDBTransaction) => R): Promise<R>; close(): void; readonly closed: boolean;}OpenIndexedDbOptions
Options for {@link openIndexedDb}.
interface OpenIndexedDbOptions { readonly version?: number; readonly upgrade?: (db: IDBDatabase, oldVersion: number, newVersion: number, transaction: IDBTransaction) => void; readonly onBlocked?: () => void; readonly onVersionChange?: () => void; readonly timeoutMs?: number;}Functions
Section titled “Functions”createStore
Creates a typed, namespaced store with TTL, versioned migrations, validation and cross-tab change events.
All methods are async regardless of backend. Falls back to memory (with persistent === false) when the
requested backend is unavailable, including during SSR. Do not keep auth tokens in web storage (XSS-readable).
function createStore<T>(options: CreateStoreOptions<T>): Store<T>;Example:
const prefs = createStore<{ density: 'compact' | 'comfortable' }>({ namespace: 'prefs', version: 2, migrate: (old) => ({ density: (old as { compact?: boolean }).compact ? 'compact' : 'comfortable' }) });await prefs.set('grid', { density: 'compact' });const stop = prefs.subscribe('grid', () => { console.info('Grid preferences changed'); });Interfaces
Section titled “Interfaces”CreateStoreOptions
Options for {@link createStore}.
interface CreateStoreOptions<T> { readonly backend?: StorageBackend; readonly namespace: string; readonly version?: number; readonly migrate?: (oldValue: unknown, fromVersion: number) => T; readonly ttlMs?: number; readonly serializer?: StoreSerializer<T>; readonly validate?: (value: unknown) => value is T; readonly onError?: (error: HelperError, key: string) => void; readonly now?: () => number;}Store
Async key-value store returned by {@link createStore}.
interface Store<T> { readonly namespace: string; readonly backend: StorageBackend; readonly persistent: boolean; readonly ready: Promise<void>; get(key: string): Promise<T | undefined>; set(key: string, value: T, options?: StoreSetOptions): Promise<void>; remove(key: string): Promise<void>; has(key: string): Promise<boolean>; keys(): Promise<string[]>; entries(): Promise<[string, T][]>; clear(): Promise<void>; prune(): Promise<number>; subscribe(key: string | null, callback: (change: StoreChange) => void): () => void; close(): void;}StoreChange
A change notification from {@link Store.subscribe}. Read the new value with get.
interface StoreChange { readonly key: string | null; readonly kind: 'set' | 'remove' | 'clear'; readonly source: 'local' | 'remote';}StoreSerializer
Converts values to and from strings.
interface StoreSerializer<T> { readonly serialize: (value: T) => string; readonly deserialize: (text: string) => unknown;}StoreSetOptions
Per-write options.
interface StoreSetOptions { readonly ttlMs?: number;}StorageBackend
Where a store keeps its data.
type StorageBackend = 'local' | 'session' | 'memory' | 'indexeddb';