A complete, production-grade IndexedDB wrapper library for the browser. Zero dependencies. Pure ES2020. Everything runs locally — no servers, no network, no backend.
Drop a single <script> tag before your app code. No bundler, no npm, no
build step required.
<!-- Local file --> <script src="https://cdn.jsdelivr.net/npm/ark-indexdb@latest/ark-indexdb.js"></script> <!-- Or inline if bundling is not available --> <script> // paste ark-indexdb.js content here </script>
// If you add export statements to ark-indexdb.js: import { ArkIndexdb } from './ark-indexdb.js';
Everything begins with new ArkIndexdb() and a single open() call
that sets up your database schema. After that you're ready for full CRUD.
// 1. Create the library instance const ark = new ArkIndexdb(); // 2. Open (or create) the database — await until ready await ark.open('MyAppDB', 1, { users: { keyPath: 'id', indexes: [ { name: 'by_email', keyPath: 'email', unique: true }, { name: 'by_role', keyPath: 'role', unique: false }, ] } }); // 3. INSERT — UUID id is generated automatically const key = await ark.insert('users', { name: 'Alice Chen', email: 'alice@example.com', role: 'admin', }); // 4. READ const user = await ark.findById('users', key); // 5. UPDATE (partial merge) await ark.update('users', key, { role: 'superadmin' }); // 6. DELETE await ark.delete('users', key);
Pass your schema as the third argument to open(). Each top-level key becomes an
object store. Indexes let you run fast native IDB lookups via findByIndex() and
findByRange().
const schema = { // Store name → definition posts: { keyPath: 'id', // primary key field (default: 'id') autoIncrement: false, // set true for auto numeric keys indexes: [ // { name, keyPath, unique } { name: 'by_author', keyPath: 'authorId', unique: false }, { name: 'by_category', keyPath: 'category', unique: false }, { name: 'by_slug', keyPath: 'slug', unique: true }, ] }, comments: { keyPath: 'id', indexes: [ { name: 'by_post', keyPath: 'postId', unique: false }, ] }, settings: { keyPath: 'key', // any field can be the key } }; await ark.open('BlogDB', 1, schema);
open('BlogDB', 2, schema)). The onupgradeneeded handler runs automatically.
| Method | Returns | Description |
|---|---|---|
| ark.open(name, version, schema) | Promise<ArkIndexdb> | Open or create the database. Always await this before any operation. |
| ark.close() | void | Close the active database connection. |
| ArkIndexdb.dropDatabase(name) | Promise<boolean> | Permanently delete a database by name (static method). |
| ArkIndexdb.listDatabases() | Promise<Array> | List all IndexedDB databases in the current origin (static method). |
| ark.isOpen | boolean | Whether the database is currently open. |
| ark.storeNames | string[] | Array of object store names. |
Insert a single record. A UUID id is auto-generated if the keyPath is
'id' and none is provided. _createdAt and _updatedAt
ISO timestamps are added automatically.
const key = await ark.insert('users', { name: 'Bob', email: 'bob@example.com', role: 'user', tags: ['beta', 'early-adopter'], }); // returns the new record's primary key (UUID string)
Insert multiple records in a single IDB transaction — much faster than looping
insert() for bulk operations.
const keys = await ark.insertMany('products', [ { name: 'Widget A', price: 9.99, category: 'widgets' }, { name: 'Widget B', price: 14.99, category: 'widgets' }, { name: 'Gadget X', price: 49.99, category: 'gadgets' }, ]); // returns string[] of inserted keys
const user = await ark.findById('users', 'abc-123'); // returns the record object, or null if not found
Retrieve all records with optional in-memory filter, sort, limit and offset.
const { data, total } = await ark.findAll('users', { filter: { role: 'admin', age: { $gte: 18 } }, sort: 'name', order: 'asc', limit: 10, offset: 0, }); // data: matching records array total: count before pagination
Uses a native IDB index for fast O(log n) lookups — ideal for indexed fields.
// Uses the native 'by_role' IDB index const { data } = await ark.findByIndex('users', 'by_role', 'admin'); // Combine with additional in-memory filtering const { data } = await ark.findByIndex( 'users', 'by_role', 'admin', { filter: { status: 'active' }, sort: 'name' } );
// Products priced between $10 and $50 const { data } = await ark.findByRange( 'products', 'by_price', { lower: 10, upper: 50 } ); // Open bounds (exclusive): price > 10 const { data } = await ark.findByRange( 'products', 'by_price', { lower: 10, lowerOpen: true } );
const result = await ark.paginate('users', 1, 10, { filter: { status: 'active' }, sort: 'name', order: 'asc' }); result.data // current page records result.total // total matching records result.totalPages // total page count result.hasNext // boolean result.hasPrev // boolean
Partial merge — only the fields you provide are changed. All other fields are preserved.
await ark.update('users', id, { name: 'Alice Smith', // update these fields city: 'Berlin', }); // all other fields (email, role, etc.) are untouched // _updatedAt is set automatically
Field-level operators — increment counters, toggle booleans, push/pull array items:
await ark.patch('users', id, { $set: { city: 'Tokyo' }, // set field $inc: { loginCount: 1 }, // increment by 1 $dec: { credits: 5 }, // decrement by 5 $toggle: { isVerified: true }, // flip boolean $push: { tags: 'vip' }, // append to array $pull: { tags: 'beta' }, // remove from array $mul: { score: 1.5 }, // multiply $unset: ['temporaryField'], // delete field });
Insert or replace — uses IDB's native put(). Does not require the record to
exist first.
// Always works — creates if missing, replaces if key exists await ark.upsert('settings', { id: 'theme', value: 'dark' }); await ark.upsert('settings', { id: 'theme', value: 'light' }); // overwrites
// Deactivate all users with role 'guest' const updated = await ark.updateWhere( 'users', { role: 'guest' }, { status: 'inactive' } ); // returns array of all updated records
// Delete by primary key await ark.delete('users', id); // Delete all matching a filter const n = await ark.deleteWhere('sessions', { expiresAt: { $lt: new Date().toISOString() } }); console.log(`Pruned ${n} expired sessions`); // Clear ALL records (keeps store structure) await ark.clear('logs');
A fluent, chainable API that reads like English. Returns a ArkQueryBuilder
instance — call .exec() to run.
// Chained query const { data } = await ark .query('users') .where({ role: 'admin', status: { $ne: 'inactive' } }) .sortBy('name', 'asc') .limit(10) .offset(0) .exec(); // Count without fetching records const n = await ark.query('users') .where({ status: 'active' }) .count(); // First matching record const admin = await ark.query('users') .where({ email: 'alice@example.com' }) .first(); // Paginate via builder const { data } = await ark.query('posts') .where({ category: 'tech' }) .sortBy('_createdAt', 'desc') .page(2, 20) // page 2, 20 per page .exec();
Run multiple operations across one or more stores atomically. If anything throws, the entire transaction rolls back automatically.
await ark.transaction( ['users', 'orders'], // stores involved 'readwrite', async (stores) => { // Both operations succeed or both roll back const userId = crypto.randomUUID(); await stores.users.add({ id: userId, name: 'Dave', _createdAt: new Date().toISOString() }); await stores.orders.add({ id: crypto.randomUUID(), userId, total: 149.99, }); } );
stores proxy exposes
promisified versions of: add, put, get,
delete, clear, getAll, count,
getAllKeys, and index(name).Subscribe to operations with ark.on(event, callback). The wildcard
'*' receives every event. Returns an unsubscribe function.
// Subscribe to insert events const off = ark.on('insert', ({ storeName, key, data }) => { console.log(`Inserted into ${storeName}: key=${key}`); }); // Wildcard — catches everything ark.on('*', e => console.log('[Ark]', e.event, e)); // Unsubscribe off(); // All available events: // 'open' 'upgrade' 'insert' 'insertMany' 'update' // 'updateMany' 'upsert' 'delete' 'deleteMany' // 'clear' 'import' 'transaction' 'error' 'versionchange'
// Export one store as JSON string const json = await ark.exportStore('users'); // Export ALL stores const fullBackup = await ark.exportAll(); // Trigger browser file download await ark.downloadStore('users'); // → browser saves "users_1700000000.json" // Import from JSON string (e.g., after fetch or file read) const json = await fetch('/backup/users.json').then(r => r.text()); const n = await ark.importStore('users', json); console.log(`Imported ${n} records`);
Use these inside filter objects with findAll(),
findOne(), updateWhere(), deleteWhere(), and the
Query Builder's .where().
| Operator | Description | Example |
|---|---|---|
| $eq | Equal to (same as plain value shorthand) | { age: { $eq: 30 } } |
| $ne | Not equal to | { status: { $ne: 'banned' } } |
| $gt | Greater than | { price: { $gt: 100 } } |
| $gte | Greater than or equal to | { age: { $gte: 18 } } |
| $lt | Less than | { stock: { $lt: 10 } } |
| $lte | Less than or equal to | { score: { $lte: 50 } } |
| $in | Value is in the given array | { role: { $in: ['admin','mod'] } } |
| $nin | Value is NOT in the given array | { status: { $nin: ['banned','deleted'] } } |
| $contains | String contains substring (case-insensitive) | { name: { $contains: 'ali' } } |
| $startsWith | String starts with prefix (case-insensitive) | { email: { $startsWith: 'admin' } } |
| $endsWith | String ends with suffix (case-insensitive) | { email: { $endsWith: '.gov' } } |
| $regex | String matches a regular expression (case-insensitive) | { phone: { $regex: '^\\+1' } } |
| $exists | Field exists (true) or is null/undefined (false) | { avatar: { $exists: true } } |
| $type | typeof field equals the given string | { score: { $type: 'number' } } |
| $size | Array field has exactly N elements | { tags: { $size: 3 } } |
{ 'address.city': 'Berlin' }Used with ark.patch() for field-level mutations without reading then writing the
full record manually.
| Operator | Type | Description |
|---|---|---|
| $set | { field: value } | Set field to the given value |
| $unset | ['field', ...] | Remove (delete) the listed fields |
| $inc | { field: n } | Add n to the field (negative n = subtract) |
| $dec | { field: n } | Subtract n from the field |
| $mul | { field: n } | Multiply the field by n |
| $toggle | { field: true } | Flip a boolean field to its opposite |
| $push | { field: value } | Append value to an array field |
| $pull | { field: value } | Remove all occurrences of value from an array field |
| Method | Returns | Description |
|---|---|---|
| open(name, version, schema) | Promise<this> | Open or create database |
| close() | void | Close connection |
| ArkIndexdb.dropDatabase(name) | Promise<boolean> | Delete a database |
| ArkIndexdb.listDatabases() | Promise<Array> | List origin's databases |
| insert(store, data) | Promise<key> | Insert one record |
| insertMany(store, records[]) | Promise<key[]> | Batch insert in one transaction |
| findById(store, id) | Promise<Object|null> | Fetch by primary key |
| findAll(store, options?) | Promise<{data, total}> | Fetch all with filter/sort/page |
| findByIndex(store, index, value) | Promise<{data, total}> | Native index lookup |
| findByRange(store, index, range) | Promise<{data, total}> | Key range query via index |
| findOne(store, filter) | Promise<Object|null> | First match or null |
| exists(store, id) | Promise<boolean> | Check record existence |
| count(store, filter?) | Promise<number> | Count records |
| getAllKeys(store) | Promise<Array> | All primary keys |
| paginate(store, page, size, opts?) | Promise<{data, total, …}> | Paginated fetch |
| update(store, id, changes) | Promise<Object> | Partial record merge |
| upsert(store, data) | Promise<key> | Insert or replace |
| patch(store, id, ops) | Promise<Object> | Field-level patch operators |
| updateWhere(store, filter, changes) | Promise<Object[]> | Bulk update by filter |
| delete(store, id) | Promise<boolean> | Delete by primary key |
| deleteWhere(store, filter) | Promise<number> | Delete all matching |
| clear(store) | Promise<number> | Delete all records in store |
| query(store) | ArkQueryBuilder | Fluent query builder |
| transaction(stores[], mode, fn) | Promise<void> | Multi-store atomic transaction |
| iterateCursor(store, cb, dir?) | Promise<void> | Memory-efficient cursor iteration |
| exportStore(store) | Promise<string> | Export store as JSON |
| exportAll() | Promise<string> | Export all stores |
| importStore(store, json) | Promise<number> | Import records from JSON |
| downloadStore(store) | Promise<void> | Browser file download |
| on(event, callback) | Function (unsubscribe) | Subscribe to events |
| off(event, callback) | void | Unsubscribe listener |