Async IndexedDB ORM, lightweight, and ready for modern runtimes.
deno add jsr:@neabyte/indexed-ormnpm install @neabyte/indexed-orm<script type="module">
import { Database } from 'https://esm.sh/jsr/@neabyte/indexed-orm'
// or pin version: .../indexed-orm@1.0.0
</script>- Latest:
https://esm.sh/jsr/@neabyte/indexed-orm - Pinned:
https://esm.sh/jsr/@neabyte/indexed-orm@<version>
You can import the following from @neabyte/indexed-orm:
- Database – open DB and run schema upgrade.
- Range – build
IDBKeyRangefrom options (e.g. for custom cursor logic). - Store – advanced: create a store handle from an existing
IDBDatabaseviaStore.createStore(db, storeName, keyPath, options?). - Types –
FindOptions,StoreHandle,OpenOptions,Builder,Chain,KeyRangeOptions,IDBValidKey,TransactionDurability, and others.
import { Database, Range, Store } from '@neabyte/indexed-orm'
import type { FindOptions, StoreHandle, OpenOptions } from '@neabyte/indexed-orm'import { Database } from '@neabyte/indexed-orm'
const db = await Database.open('MyApp', 1, builder => {
builder.store('users', 'id').index('email')
})
const users = db.store('users')
await users.put({ id: '1', email: 'a@b.com', name: 'Alice' })
const user = await users.get('1')Open a database by name and version. The upgrade callback runs on first open and on version bumps. The second argument to store() is the key path (e.g. 'id') — the property on your objects used as the primary key.
Version and data: Bumping the version (e.g. 1 → 2) only reruns the upgrade callback so you can create new stores or indexes. Existing data is kept unless you explicitly delete object stores or clear them inside upgrade. Data is lost only if you drop a store or the user clears site data.
import { Database } from '@neabyte/indexed-orm'
const db = await Database.open('MyApp', 1, builder => {
builder.store('users', 'id').index('email').index('name')
builder.store('posts', 'id').index('userId').index('createdAt')
})
// Optional: openOptions (e.g. relaxed durability for better write perf where allowed)
const dbRelaxed = await Database.open(
'MyApp',
1,
builder => {
builder.store('users', 'id')
},
{ durability: 'relaxed' }
)Get a store handle (db is a DBHandle):
const users = db.store('users')// add(item, key?): with keyPath store, the key is taken from item[keyPath]; pass key only to override or when the item has no keyPath property
await users.add({ id: '1', email: 'a@b.com', name: 'Alice' })
await users.put({ id: '1', email: 'a@b.com', name: 'Alice Updated' })
// Read, update, delete
const user = await users.get('1')
await users.update('1', { name: 'Alice Smith' })
await users.delete('1')
// Bulk and clear
await users.putMany([
{ id: '1', email: 'a@b.com' },
{ id: '2', email: 'b@b.com' }
])
await users.deleteMany(['1', '2'])
await users.clear()Basic find with index, prefix, ordering, limit, and offset:
const out = await users.find({
index: 'email',
prefix: 'a',
order: 'asc',
limit: 10,
offset: 0
})Range queries (eq, min, max, includeMin, includeMax). Use eq with no index to match by primary key; use index to match by that index:
await users.find({ eq: '1' })
await users.find({ index: 'email', eq: 'alice@b.com' })
await users.find({ index: 'createdAt', min: 1000, max: 2000, includeMin: true, includeMax: false })Multiple prefix match (prefixes — match if key starts with any of the given strings):
await users.find({ index: 'email', prefixes: ['alice', 'admin'] })Additional filters:
where— Client-side only. The predicate runs in JS while iterating or filtering results; it is not pushed to IndexedDB indexes. Use it for flexible conditions (e.g.item.role === 'admin'), but on large datasets index-based options (eq,prefix,min/max) are faster because they use the engine’s range scan.ignoreCase— Applies only toeq,prefix, andprefixes(case-insensitive string comparison). It does not affectmin/maxor other range options. Comparison is done in JS after the range is applied.
const out = await users.find({
anyOf: ['id1', 'id2'],
noneOf: ['id3'],
notEq: 'id4',
where: item => item.role === 'admin',
distinct: true,
ignoreCase: true
})For custom key ranges (e.g. multiple intervals), use the ranges option with pre-built IDBKeyRange objects — build them with Range.buildKeyRange(opts).
await users.count({ prefix: 'a' })
await users.first({ order: 'asc' })
await users.last({ order: 'desc' })
await users.keys({ limit: 100 })
await users.primaryKeys({ limit: 100 })
await users.uniqueKeys('email', {})Return false from the callback to stop iteration:
await users.each(item => {
if (item.id === 'stop') {
return false
}
})
await users.eachKey(key => {
console.log(key)
})
await users.eachUniqueKey('email', key => {
console.log(key)
})modify updates records by cursor. Return a partial object to merge into each record, or false to stop iteration. Each modify call runs inside a single readwrite transaction (atomic). Two overlapping modify calls are separate transactions; IndexedDB does not lock across them, so if two modify calls touch the same records at the same time, the last write wins. For predictable results, avoid concurrent modify on the same store or sequence writes in your app.
await users.modify(item => ({ ...item, role: 'user' }), { where: item => item.role === 'guest' })Each store method uses its own transaction (e.g. one putMany = one readwrite transaction; one find = one readonly transaction). The ORM does not expose cross-store or custom multi-operation transactions. If you need a single atomic transaction across multiple stores (e.g. deduct from accounts and append to ledger), use raw IndexedDB: open the database, start a transaction with multiple store names, and perform the operations on transaction.objectStore(...).
If you already have an IDBDatabase (e.g. from another library or custom indexedDB.open()), you can create a store handle without Database.open. The database must already have the object store (e.g. created in onupgradeneeded).
import { Store } from '@neabyte/indexed-orm'
const rawDb = await new Promise<IDBDatabase>((resolve, reject) => {
const r = indexedDB.open('MyApp', 1)
r.onupgradeneeded = () => {
r.result.createObjectStore('users', { keyPath: 'id' })
}
r.onsuccess = () => resolve(r.result)
r.onerror = () => reject(r.error)
})
const users = Store.createStore(rawDb, 'users', 'id')
// same StoreHandle API: users.get(), users.find(), etc.All store methods return Promises. Rejections can come from IndexedDB (e.g. quota exceeded, database blocked by user, or transaction aborted). For robust apps, wrap calls in try/catch and handle known cases:
try {
await users.put(largeItem)
} catch (e) {
if (e.name === 'QuotaExceededError') {
// Storage full; prompt user or evict old data
}
if (e.name === 'SecurityError' || e.name === 'UnknownError') {
// e.g. private browsing or user disabled storage
}
throw e
}Errors from Database.open (e.g. version conflict or blocked) also need handling if you support multiple tabs or strict reliability.
| Method | Category | Description |
|---|---|---|
Database.open |
Database | Opens DB and runs upgrade callback. |
DBHandle.store |
Database | Returns store handle by name. |
Store.createStore |
Store | Returns store handle from existing IDBDatabase. |
StoreHandle.add |
StoreHandle | Adds item; optional key when applicable. |
StoreHandle.put |
StoreHandle | Puts item (overwrites by key). |
StoreHandle.get |
StoreHandle | Gets one item by key. |
StoreHandle.update |
StoreHandle | Merges changes into item at key. |
StoreHandle.delete |
StoreHandle | Deletes item by key. |
StoreHandle.putMany |
StoreHandle | Puts many items in one transaction. |
StoreHandle.deleteMany |
StoreHandle | Deletes many keys in one transaction. |
StoreHandle.clear |
StoreHandle | Clears all items in store. |
StoreHandle.find |
StoreHandle | Finds items matching FindOptions. |
StoreHandle.count |
StoreHandle | Counts items matching FindOptions. |
StoreHandle.first |
StoreHandle | Returns first item matching options. |
StoreHandle.last |
StoreHandle | Returns last item matching options. |
StoreHandle.keys |
StoreHandle | Returns primary keys matching options. |
StoreHandle.primaryKeys |
StoreHandle | Alias for keys. |
StoreHandle.uniqueKeys |
StoreHandle | Returns deduped keys from index. |
StoreHandle.each |
StoreHandle | Iterates matching items; stop with false. |
StoreHandle.eachKey |
StoreHandle | Iterates matching keys; stop with false. |
StoreHandle.eachUniqueKey |
StoreHandle | Iterates index keys; stop with false. |
StoreHandle.modify |
StoreHandle | Cursor update; returns updated count. |
Range.buildKeyRange |
Range | Builds IDBKeyRange from range options. |
Database.open(name, version, upgrade?, openOptions?)name<string>: Database name.version<number>: Schema version; bump to rerunupgrade.upgrade<(db: Builder) => void>: (Optional) Defines stores and indexes.openOptions<OpenOptions>: (Optional){ durability?: 'default' | 'relaxed' }. Defaults to{}.- Returns:
Promise<DBHandle> - Description: Opens DB, runs upgrade, and returns handle.
db.store(storeName)storeName<string>: Object store name.- Returns:
StoreHandle<unknown> - Description: Returns store handle bound to store name.
Store.createStore(db, storeName, keyPath, options?)db<IDBDatabase>: Opened IndexedDB database.storeName<string>: Object store name.keyPath<string>: Primary key path (e.g.'id').options<CreateStoreOptions>: (Optional){ durability?: 'default' | 'relaxed' }.- Returns:
StoreHandle<T> - Description: Creates a store handle when you already have an
IDBDatabase(e.g. from customindexedDB.open()or another library).
store.add(item, key?)item<T>: Item to add.key<IDBValidKey>: (Optional) The key is taken fromitem[keyPath], so usually omitted. Passkeyonly to override that key or when the item does not have the keyPath property.- Returns:
Promise<IDBValidKey> - Description: Adds item; fails if a record with the same key already exists (use
putto overwrite).
store.put(item)item<T>: Item with keyPath field.- Returns:
Promise<IDBValidKey> - Description: Puts item; overwrites existing item with same key.
store.putMany(items)items<T[]>: Array of items to put.- Returns:
Promise<void> - Description: Puts multiple items in one transaction; no-op when not an array.
store.get(key)key<IDBValidKey>: Primary key.- Returns:
Promise<T | undefined> - Description: Returns one item by key, or undefined when missing.
store.update(key, changes)key<IDBValidKey>: Primary key.changes<Partial<T>>: Fields to merge into existing item.- Returns:
Promise<void> - Description: Gets existing item, merges changes, puts back; no-op when key missing.
store.delete(key)key<IDBValidKey>: Primary key to delete.- Returns:
Promise<void> - Description: Deletes item by key; no-op when key null or undefined.
store.deleteMany(keys)keys<IDBValidKey[]>: Array of keys to delete.- Returns:
Promise<void> - Description: Deletes multiple keys in one transaction; no-op when not an array.
store.clear()- Returns:
Promise<void> - Description: Clears all items in the store in one transaction.
store.find(options?)options<FindOptions<T>>: (Optional) Filter and query options. Defaults to{}.- Returns:
Promise<T[]> - Description: Returns items matching options and filters. Options like
eq,prefix,min/maxuse the index/range;where,anyOf,noneOf,distinct,ignoreCaseare applied client-side (see Option Types).
store.count(options?)options<FindOptions<T>>: (Optional) Same filters as find.- Returns:
Promise<number> - Description: Counts items matching options; uses range count when possible.
store.first(options?)options<FindOptions<T>>: (Optional) Same filters as find; order applies.- Returns:
Promise<T | undefined> - Description: Returns first item matching options, or undefined.
store.last(options?)options<FindOptions<T>>: (Optional) Same filters as find; order applies.- Returns:
Promise<T | undefined> - Description: Returns last item matching options, or undefined.
store.keys(options?)options<FindOptions<T>>: (Optional) Same filters as find.- Returns:
Promise<IDBValidKey[]> - Description: Returns primary keys matching options.
store.primaryKeys(options?)options<FindOptions<T>>: (Optional) Same as keys.- Returns:
Promise<IDBValidKey[]> - Description: Alias for
keys; returns primary keys matching options.
store.uniqueKeys(indexName?, options?)indexName<string>: (Optional) Index name; when omitted, behaves like keys.options<FindOptions<T>>: (Optional) Range and filter options.- Returns:
Promise<IDBValidKey[]> - Description: Returns deduped keys from index; falls back to keys when indexName omitted.
store.each(callback, options?)callback<(item: T) => void | false>: Invoked per item; return false to stop.options<FindOptions<T>>: (Optional) Same filters as find.- Returns:
Promise<void> - Description: Iterates matching items; return false from callback to stop.
store.eachKey(callback, options?)callback<(key: IDBValidKey) => void | false>: Invoked per key; return false to stop.options<FindOptions<T>>: (Optional) Same filters as find.- Returns:
Promise<void> - Description: Iterates matching keys only; return false to stop.
store.eachUniqueKey(indexName, callback, options?)indexName<string>: Index name (required).callback<(key: IDBValidKey) => void | false>: Invoked per unique key; return false to stop.options<FindOptions<T>>: (Optional) Range options.- Returns:
Promise<void> - Description: Iterates unique keys of index; key cursor; indexName required.
store.modify(changeCallback, options?)changeCallback<(item: T) => void | Partial<T> | false>: Callback per item; object merges;falsestops.options<FindOptions<T>>: (Optional) Select items to modify. Defaults to{}.- Returns:
Promise<number> - Description: Updates records by cursor; returns updated count.
Range.buildKeyRange(opts)opts<KeyRangeOptions>: Key range options.- Returns:
IDBKeyRange | null - Description: Builds
IDBKeyRangefor cursor queries.
- IDBValidKey: key type for get, delete, add( key ), etc. —
string | number | Date | ArrayBufferView | ArrayBuffer. - OpenOptions (optional):
durability('default' | 'relaxed'). - CreateStoreOptions (optional):
durability('default' | 'relaxed'). - Builder (passed to
Database.openupgrade):store(storeName, keyPath)→ returnsChain; use to define stores and indexes. - Chain (from
builder.store()):index(indexName)→ returnsChain; add index names for the store. - KeyRangeOptions (optional):
eq,min,max,includeMin,includeMax,prefix. - FindOptions (optional):
index,order,eq,notEq,min,max,includeMin,includeMax,prefix,prefixes,ignoreCase(foreq/prefix/prefixesonly),anyOf,noneOf,where(client-side filter),distinct,ranges,limit,offset. - StoreSchemaConfig (internal):
{ keyPath, indexes }— schema shape per store (from upgrade).
- IndexedDB API — MDN Web APIs
This project is licensed under the MIT license. See the LICENSE file for details.