Developer Docs

IndexedDB Storage Layer

The $lib/idb.ts module provides a lightweight, promise-based wrapper around the browser's IndexedDB API. It replaces localStorage for stores that may accumulate significant data over time โ€” wishlists with hundreds of saves, notification preferences, recently viewed history, and uChat unread timestamps.

Why IndexedDB?

Aspect localStorage IndexedDB
Size limit ~5 MB per origin ~50+ MB (browser-dependent)
API Synchronous (blocks main thread) Asynchronous (non-blocking)
Data model String key-value only Structured cloning (objects, arrays, blobs)
Transactions None Full ACID transactions
Querying Manual JSON.parse on every read Native indexing and cursors

For stores holding a few small values (auth tokens, theme preference, org ID), localStorage remains appropriate. For stores that may grow with user activity, IndexedDB is the correct choice.

Architecture

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚              Svelte Stores (reactive)             โ”‚
โ”‚                                                    โ”‚
โ”‚  favorites.ts  recentlyViewed.ts  uchatStore.ts   โ”‚
โ”‚  wishlistStore.ts                                  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                โ”‚                    โ”‚
                โ–ผ                    โ–ผ
        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
        โ”‚         $lib/idb.ts             โ”‚
        โ”‚  idbGet / idbSet / idbDelete    โ”‚
        โ”‚  idbClear / migrateFromLS       โ”‚
        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                        โ”‚
                        โ–ผ
              โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
              โ”‚   IndexedDB      โ”‚
              โ”‚  "mallnline_store"โ”‚
              โ”‚                  โ”‚
              โ”‚  โ”Œโ”€ favorites    โ”‚
              โ”‚  โ”œโ”€ recently_viewedโ”‚
              โ”‚  โ”œโ”€ notifications โ”‚
              โ”‚  โ”œโ”€ navigation   โ”‚
              โ”‚  โ””โ”€ uchat_unread โ”‚
              โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

API Reference

`idbGet(store, key?)`

Read a value from IndexedDB.

import { idbGet, IDB_STORES } from '$lib/idb';

const items = await idbGet<string[]>(IDB_STORES.FAVORITES);
// โ†’ ['luminara-crafts', 'pixel-forge'] or undefined

Parameters:

Param Type Default Description
store StoreName โ€” Object store name (from IDB_STORES)
key string 'data' Key within the store

Returns T | undefined. Resolves to undefined if the key doesn't exist or IndexedDB is unavailable.

`idbSet(store, value, key?)`

Write a value to IndexedDB (upsert semantics).

import { idbSet, IDB_STORES } from '$lib/idb';

await idbSet(IDB_STORES.FAVORITES, ['luminara-crafts', 'pixel-forge']);

Fire-and-forget pattern โ€” the calling store updates its reactive state synchronously, then persists asynchronously.

`idbDelete(store, key?)`

Delete a single key from an object store.

import { idbDelete, IDB_STORES } from '$lib/idb';

await idbDelete(IDB_STORES.RECENTLY_VIEWED, 'data');

`idbClear(store)`

Clear all entries in an object store.

import { idbClear, IDB_STORES } from '$lib/idb';

await idbClear(IDB_STORES.RECENTLY_VIEWED);

`migrateFromLocalStorage(lsKey, store, idbKey?, parser?)`

One-time migration utility. Reads from localStorage, writes to IndexedDB, then removes the localStorage key. Idempotent โ€” subsequent calls are no-ops.

import { migrateFromLocalStorage, IDB_STORES } from '$lib/idb';

const migrated = await migrateFromLocalStorage<string[]>(
  'malet_favorites',       // localStorage key
  IDB_STORES.FAVORITES     // target IDB store
);
// If migration occurred: returns parsed data
// If no localStorage key: returns undefined

IMPORTANT

The localStorage key is only removed after a successful IndexedDB write. If IndexedDB is unavailable, the migration fails silently and localStorage data is preserved.

Object Store Registry

export const IDB_STORES = {
  FAVORITES: 'favorites',          // Wishlist items + malet follows
  RECENTLY_VIEWED: 'recently_viewed',  // Recent malet visit history
  NOTIFICATIONS: 'notifications',  // Notification preferences (future)
  NAVIGATION: 'navigation',       // Sidebar state (future)
  UCHAT_UNREAD: 'uchat_unread'     // Last-read timestamps per conversation
} as const;

All stores are created in a single onupgradeneeded handler when the database is first opened (version 1).

Migrated Stores

`favorites.ts`

Before: localStorage.getItem('malet_favorites') After: IDB_STORES.FAVORITES / key 'data'

The favorites store now uses idbGet for hydration at load and idbSet for persistence on follow/unfollow. On first load, migrateFromLocalStorage checks for existing localStorage data and moves it to IndexedDB.

`recentlyViewed.ts`

Before: localStorage.getItem('recently_viewed_malets') After: IDB_STORES.RECENTLY_VIEWED / key 'data'

Same migration pattern. The store loads asynchronously from IndexedDB on import (browser only), with a loadFromIDB() function that handles migration.

`uchatStore.svelte.ts`

Before: localStorage.getItem('uchat_last_read') After: IDB_STORES.UCHAT_UNREAD / key 'last_read'

The unread tracking timestamps (Record<conversationId, isoTimestamp>) are migrated to IndexedDB. The store initializes the lastReadTimestamps state from IDB in an async IIFE on module load.

What Stays on localStorage

Key Store Rationale
auth auth.ts Synchronous read needed on page load for auth checks
theme themeStore.ts Synchronous read to prevent FOUC (flash of unstyled content)
guestId auth.ts Tiny string, read synchronously for anonymous tracking
orgId orgs.ts Tiny string, read synchronously for header injection

These values are small (< 100 bytes each) and require synchronous access โ€” making localStorage the correct choice.

Design Principles

  1. Graceful degradation โ€” If IndexedDB is unavailable (e.g., private browsing in some engines), all operations return undefined or resolve silently. No errors thrown.
  2. Single database โ€” One database (mallnline_store) with multiple object stores. Avoids connection overhead.
  3. Cached connection โ€” The openDB() promise is memoized. Only one connection per page lifecycle.
  4. Fire-and-forget writes โ€” UI updates synchronously via Svelte stores. IndexedDB syncs asynchronously. Users never wait for persistence.
  5. Automatic migration โ€” Existing localStorage data is transparently migrated on first access. Users don't notice the transition.