IndexedDB

IndexedDB is a transactional database in every browser (Baseline Widely available). Object stores, its tables, hold any structured-cloneable value, Blobs included. Keys come from a keyPath property or autoIncrement; indexes on other properties support lookups and IDBKeyRange queries. Stores and indexes are created only in upgradeneeded, fired when the database is opened with a higher version. All work runs in readonly or readwrite transactions that commit once no requests are pending.

The raw API reports results through request events; Jake Archibald's idb 7,416 (https://github.com/jakearchibald/idb 7,416 ) (8.0.3, ISC license, npm 2,036 install idb) wraps each request in a promise and changes little else:

An indexed student database with idbJavaScript
import { openDB } from 'https://cdn.jsdelivr.net/npm/idb@8/+esm';
const db = await openDB('school', 1, {
  upgrade: (db) => db.createObjectStore('students', { keyPath: 'id' }).createIndex('clss', 'clss'),
});
const tx = db.transaction('students', 'readwrite');
await Promise.all([
  tx.store.put({ id: 'H423498', name: 'Mike', clss: '5D', age: 11 }),
  tx.store.put({ id: 'A213132', name: 'Bill', clss: '4H', age: 10 }),
  tx.store.put({ id: 'H234324', name: 'Paul', clss: '3A', age: 7 }),
  tx.done,                                        // resolves when the transaction commits
]);
const range = IDBKeyRange.bound('1A', '4H');     // classes 1A..4H, inclusive
const kids = await db.getAllFromIndex('students', 'clss', range);
console.log(kids.map((s) => `${s.name} ${s.clss}`).join(', '));
Output
Paul 3A, Bill 4H

Dexie.js (https://github.com/dexie/Dexie.js 14,607 ) (4.4.6, Apache-2.0, npm install dexie) declares the schema as a string, db.version(1).stores({ students: 'id, clss, age' }), queries fluently, db.students.where('age').below(11).sortBy('name'), and offers a useLiveQuery() React 7,897 hook. For plain get and set, idb-keyval 3,249 (https://github.com/jakearchibald/idb-keyval 3,249 ) (6.3.0) is enough.

Beware TransactionInactiveError: awaiting a non-IndexedDB promise such as fetch() lets the transaction commit, so fetch first, then write. Close the connection on versionchange, or another tab's upgrade stays blocked.