npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

web-idb-client

v1.0.1

Published

Browser-only typed IndexedDB client with a Prisma-like store API and schema-in-TypeScript factories.

Readme

IndexedDbClient

Typed IndexedDB client for the browser (web-idb-client) with a Prisma-like store API, schema-in-TypeScript factories, and explicit migrations.

🇷🇺 Читать на русском языке

Features

  • Schema via defineStore / field.*InferSelect / InferInsert without codegen
  • Store API: insert, find, findMany, findUnique, update, delete, count, clear
  • Index-only where (equality + ranges) and orderBy / take / skip
  • Multi-store transaction('r' | 'rw', stores, fn)
  • Versioned open with schema diff + optional upgrade hooks
  • Opt-in liveQuery plugin for same/cross-tab UI invalidation

When to use / When not to use

Use this library when you need a typed, Prisma-shaped API over real IndexedDB in the browser — typically for private or corporate web apps: internal SPAs, account cabinets, form drafts, client-side caches, multi-step wizards with local state. Such cases exist, but they are relatively uncommon; pick this library deliberately, not as a default “better localStorage”.

Do not use this library when you need:

  • Mobile / React Native as the primary platform — use mature options there (e.g. WatermelonDB, RxDB)
  • Offline-first sync / replication across devices — that is a different product category (e.g. RxDB)
  • A battle-tested IndexedDB ecosystem with plugins, liveQuery, and years of production hardening — prefer Dexie
  • A minimal get/put/cursor wrapper with no ORM surface — prefer idb
  • SQL in the browser (WASM SQLite/Postgres) — prefer Drizzle/Kysely-style stacks

Platform: browser IndexedDB only. Not aimed at Node (unless you polyfill IndexedDB) and not planned as a mobile SDK.

Installation

npm install web-idb-client

Quick start

import {
  createClient,
  defineStore,
  field,
  type InferSelect,
} from 'web-idb-client';

const tasks = defineStore('tasks', {
  id: field.number().primaryKey({ autoIncrement: true }),
  name: field.string().index('byName'),
  isDone: field.boolean().index('byDone'),
  createdAt: field.date().index('byCreatedAt'),
});

type Task = InferSelect<typeof tasks>;

const db = createClient({
  name: 'myApp',
  version: 1,
  stores: { tasks },
});

await db.open();

const id = await db.tasks.insert({
  name: 'Learn IndexedDB',
  isDone: false,
  createdAt: new Date(),
});

const task: Task | null = await db.tasks.find({ where: { id } });

const byName = await db.tasks.findMany({
  where: { name: 'Learn IndexedDB' },
});

const newest = await db.tasks.findMany({
  orderBy: { createdAt: 'desc' },
  take: 10,
});

await db.tasks.update({
  where: { id },
  data: { isDone: true },
});

await db.transaction('rw', ['tasks'], async (tx) => {
  await tx.tasks.insert({
    name: 'In a transaction',
    isDone: false,
    createdAt: new Date(),
  });
});

Schema

import { defineStore, compoundIndex, field } from 'web-idb-client';

const tasks = defineStore(
  'tasks',
  {
    id: field.number().primaryKey({ autoIncrement: true }),
    title: field.string(),
    status: field.string().index('byStatus'),
    tags: field.array(field.string()).multiEntry('byTags'),
    userId: field.string(),
    dueAt: field.date().optional(),
  },
  () => [compoundIndex('byUserDue', ['userId', 'dueAt'])],
);

Field helpers: field.string, field.number, field.boolean, field.date, field.json, field.array.
Chain: .optional(), .primaryKey({ autoIncrement? }), .ref(store), .index(name), .uniqueIndex(name), .multiEntry(name).

Only PK and fields with .index() / .uniqueIndex() / .multiEntry() / compoundIndex() become IndexedDB indexes. Other fields are typed for insert/select only.

FK fields: prefer .ref(users).index('byUserId') (or .ref('users')) then loadMany / loadOne; relation endpoints via end(users, 'id') — see docs/relations.md. There is no Prisma-style include.

Boolean fields and indexes

IndexedDB keys cannot be true / false. For every field.boolean() (indexed or not) the client:

  • accepts and returns boolean in the TypeScript API (insert, where, findMany, …);
  • stores 0 | 1 in the object store so indexes like .index('byDone') work.

In DevTools / Application → IndexedDB you will see numbers (0 / 1), not booleans. The app still gets boolean. Legacy rows that already contain true/false are decoded on read; rewrite them to 0|1 on upgrade if you add a boolean index later.

Store methods

| Method | Returns | Notes | | --- | --- | --- | | insert(data) | IDBValidKey | add; autoIncrement PK optional on insert | | insertMany(data[]) | IDBValidKey[] | Sequential adds on one store tx | | find({ where, orderBy? }) | T \| null | First match (Prisma findFirst style) | | findMany({ where?, orderBy?, take?, skip?, sortInMemory? }) | T[] | List | | findUnique({ where }) | T \| null | PK or unique index only | | update({ where, data }) / updateMany | number | Rows modified | | delete({ where }) / deleteMany | number | Rows deleted | | count({ where? }) | number | | | clear() | void | Clears the store |

where must target the primary key or a declared index (field name or index name).

Range operators (map to IDBKeyRange): equals, gt / gte / lt / lte, aliases above / aboveOrEqual / below / belowOrEqual, startsWith (string prefix), between: [lo, hi] (optional open flags).

Compound indexes:

// exact key
where: { byUserDue: [userId, dueAt] }
// exact prefix + range on the last part only
where: { byUserDue: [userId, { gte: dueAt }] }
// or object form matching the compound keyPath fields
where: { userId, dueAt: { between: [a, b] } }

If where and orderBy use different indexes, pass sortInMemory: true (fetches the where set, then sorts in JS — can be expensive). Otherwise the client throws QUERY_INVALID.

Bulk helpers: insertMany, updateMany, deleteMany (the last two are aliases of multi-row update / delete).

Client methods

| Method | Notes | | --- | --- | | open() / init() | Opens DB; creates/upgrades stores & indexes | | close() | Closes connection | | deleteDatabase() | Deletes the whole DB | | transaction(mode, storeNames, fn) | mode: 'r' | 'rw'; fn receives typed stores on one IDBTransaction |

Also available: new IndexedDbClient({ name, version, stores }) (same config as createClient).

Upgrade hooks (keyed by target version):

const db = createClient({
  name: 'myApp',
  version: 2,
  stores: { tasks },
  upgrade: {
    2: (tx) => {
      // data transforms during versionchange on `tx`
    },
  },
});

Changing a store keyPath in place is rejected with SCHEMA_MISMATCH unless you recreate the store in an upgrade hook via copyStoreAndDrop — see docs/upgrades.md.

Errors

Thrown as IndexedDbError with code: NOT_OPEN, SCHEMA_INVALID, SCHEMA_MISMATCH, QUERY_INVALID, TRANSACTION_FAILED, OPEN_FAILED, BLOCKED.
Optional context fields: store, index, field (also appended to message).

const db = createClient({
  name: 'myApp',
  version: 1,
  stores: { tasks },
  // Reject unknown keys on insert (helpful in corp TS codebases)
  strictInsert: true,
});

Breaking changes from @azarov-serge/indexed-db-client

  • Install / import web-idb-client (this package)
  • Removed from().select / string-based config (dbName, storageNames, storeNameToIndexes)
  • Use defineStore + createClient / new IndexedDbClient({ name, version, stores })
  • Prefer find / findMany instead of select

Docs

Examples

Overview: examples/README.md5 Pages routes (hub + 4 demos). Each demo uses src/db/{stores,db,helpers}.ts.

| Page | Command | Covers | | --- | --- | --- | | home (/) | npm run home:dev | Links + short blurb for each demo | | todo (/todo/) | npm run demo:dev | CRUD UI, boolean indexes, transaction, liveQuery | | relations | npm run relations:dev | ref / loadMany / loadOne | | queries | npm run queries:dev | Ranges, compound / multiEntry, bulk, strictInsert | | upgrades | npm run upgrades:dev | copyStoreAndDrop, Store.migrate |

Assemble for GitHub Pages: npm run pages:builddist-pages/.

Guides: upgrades · relations · liveQuery

License

MIT