Prerequisites: Since you're reading this article, I assume that: 1. You're doing front end work, and need something similar to localStorage but more powerful. 2. You found may be the answer (which is correct), and did some research. Now you know what it is, what it does, etc. (If not, I recommend from Microsoft) IndexedDB this video 3. Unfortunately, you also find that IndexedDB has a bad reputation of being hard to use - the native APIs are not friendly at all. 4. Then you found , the most popular package about IndexedDB on npm. (below is the comparison of popularity in the recent 1 year:) idb (see live chart at ) npmcharts.com My promises: If you read this article , I promise that: slowly and carefully, line by line 1. You won't need other tutorials, this one is all you need. 2. You can learn IndexedDB by using idb, no need to touch the native APIs during the process. 3. You'll understand all the important concepts of IndexedDB, and become mentally comfortable using it. The concepts is a bigger barrier than the syntax. Getting started: First, open (everything we'll do today is included here), then click . this CodeSandbox demo1 Below is the equivalent of demo1. If you prefer playing locally, just copy this code, and find a way to run the function (I recommend attaching it to a button, because more demos are coming). { openDB } ; { openDB( , , { upgrade(db) { db.createObjectStore( ); db.createObjectStore( ); }, }); openDB( , , { upgrade(db) { db.createObjectStore( , { : }); db.createObjectStore( , { : }); }, }); } import from 'idb' // demo1: Getting started export ( ) function demo1 'db1' 1 'store1' 'store2' 'db2' 1 'store3' keyPath 'id' 'store4' autoIncrement true Don't worry about reading the code now, just run it. Then, open chrome DevTools and find localStorage. Underneath it you'll see , and see we've just created 2 DBs and 4 stores: IndexedDB (on my CodeSandbox, you'll see a few other DBs already created by the website. Ignore those, we only care about db1 and db2) This is a typical structure you may have in production - the project is big and complicated, and you organize things into different stores under different DBs like this screenshot. Each store is like a localStorage on steroid, where you store key-value pairs. If you only need 1 localStorage on steroid inside 1 db, check out the package idb-keyval by the same creator of idb. You may not need to continue reading. Easy things first: how to insert and retrieve data? Now that we've had some stores, let's put in some data. It's actually more complicated than demo1 to create DBs and create stores, so we'll talk about those later. Let's run demo2 (again, either click on , or copy the code below and run locally): demo2 my CodeSandbox { openDB } ; { db1 = openDB( , ); db1.add( , , ); db1.add( , , ); db1.close(); } import from 'idb' // demo2: add some data into db1/store1/ export async ( ) function demo2 const await 'db1' 1 'store1' 'hello world' 'message' 'store1' true 'delivered' Then in DevTools, hit the refresh button and see what changed: Sure enough, our data has been put in! (Note that appears before , that's because the store is always auto sorted by key, no matter what order you insert them in) delivered: true message: 'hello world' Let's look at the syntax of demo2: when you need to do something to a store, you first need to "connect to the db" by calling , which returns the db object, then you can call its methods. In VSCode, the intellisense will help you with the methods: after you type , "add" will pop up; after you type , the parameters will pop up. openDB() db1. db1.add( There are two questions you must be asking right now: What the heck is the argument ? And why is keyName the last argument? Answers will show up in later sections, for now let's continue our demos: 1 demo3: error handling { db1 = openDB( , ); db1 .add( , , ) .then( { .log( , result); }) .catch( { .error( , err); }); db1.close(); } // demo3: error handling export async ( ) function demo3 const await 'db1' 1 'store1' 'hello again!!' 'new message' => result console 'success!' => err console 'error: ' returns a promise, so you can implement your own error handling. When you run demo3, "success!" will show in the console, but if you run it once again, an error will show, because keys must be unique in a store. db1.add() In DevTools, there are 2 buttons to delete one entry or clear all entries in a store, use these 2 buttons to repeat demo3 to test your errors: In code, the equivalent of these two buttons are and . Again, intellisense will help you. db.clear(storeName) db.delete(storeName, keyName) A few things regarding db.close(): Q: Do I have to open and close the db every time I do something? A: For demo purposes, snippets in this article all start with openDB() to establish a connection, and ends with db.close(). However, in reality, the typical pattern is to establish a single connection to use over and over without ever closing it, for example: { openDB } ; idb = { : openDB( , ), : openDB( , ) }; import from "idb" export const db1 "db1" 1 db2 "db2" 1 Then, to use it: { idb } ; { ( idb.db1).add( , value, key); } import from "../idb" export async ( ) function addToStore1 key, value await "store1" This way you don't need to open and close the db every time. Q: Can I open multiple connections to the same db? A: Yes, if you call openDB() at multiple places in your codebase, you'll have multiple open connections at the same time, and that's fine. You don't even have to remember to close them, although that wouldn't feel nice. Q: In demo3, db.add() is asynchronous. Why did you call db.close() before things are finished? A: Calling db.close() won't close the db immediately. It'll wait until all queued operations are completed before closing. demo4: auto generate keys: Now let's answer a question we asked previously: why is the last argument? The answer is "because it can be omitted". key If you go back to demo1, you'll see that when we created store3 and store4, we gave the option to store3, and to store4. Now let's try adding some cats into store3 and store4: { keyPath: 'id' } { autoIncrement: true } { db2 = openDB( , ); db2.add( , { : , : , : }); db2.add( , { : , : , : }); db2.add( , { : , : , : }); db2.add( , { : , : , : }); db2.close(); } // demo4: auto generate keys: export async ( ) function demo4 const await 'db2' 1 'store3' id 'cat001' strength 10 speed 10 'store3' id 'cat002' strength 11 speed 9 'store4' id 'cat003' strength 8 speed 12 'store4' id 'cat004' strength 12 speed 13 We omitted the last argument in this demo. Run it and you'll see that the ids become the keys in store3 , and auto incremented integers become keys in store4. As you just found, numbers can be key. Actually in IndexedDB, dates, binaries, and arrays can also be key. With auto generated keys, store3 and store4 look less like localStorage on steroid now, and more like traditional databases. demo5: retrieve values: The syntax of retrieving values are self-explanatory, you can run demo5 and watch the results in console log: { db2 = openDB( , ); db2.get( , ).then( .log); db2.getAll( ).then( .log); db2.count( ).then( .log); db2.getAllKeys( ).then( .log); db2.close(); } // demo5: retrieve values: export async ( ) function demo5 const await 'db2' 1 // retrieve by key: 'store3' 'cat001' console // retrieve all: 'store3' console // count the total number of items in a store: 'store3' console // get all keys: 'store3' console demo6: to set a value: Use instead of if you want to update / overwrite an existing value. If the value didn't exist before, it'd be the same as add(). db.put() db.add() { db1 = openDB( , ); db1.put( , , ); db1.close(); db2 = openDB( , ); db2.put( , { : , : , : }); db2.close(); } // demo6: overwrite values with the same key export async ( ) function demo6 // set db1/store1/delivered to be false: const await 'db1' 1 'store1' false 'delivered' // replace cat001 with a supercat: const await 'db2' 1 'store3' id 'cat001' strength 99 speed 99 In RESTful APIs, PUT is "idempotent" (POST is not), meaning you can PUT something multiple times, and it'll always replace itself, whereas POST will create a new item every time. Put has the same meaning in IndexedDB, so you can run demo6 as many times as you want. If you used add() instead of put(), error would occur because you're trying to add a new item with an existing key, and keys must be unique. Transactions: In database terms, a "transaction" means several operations are executed as a group, changes to the database only get committed if all steps are successful. If one fails, the whole group is aborted. The classic example is a transaction of 1000 dollars between two bank accounts, where A+=1000 and B-=1000 must both succeed or both fail. Every operation in IndexedDB must belong to a transaction. In all the demos above, we had been making transactions all along, but all of them were single-action transactions. For instance, when we added 4 cats in demo4, we actually created 4 transactions. To create a transaction containing multiple steps that either all success or all fail, we need to write it manually: demo7: multiple operation within one transaction: Now let's move our super cat from store3 to store4 by adding it to store4 and deleting it in store3. These two steps must either both succeed or both fail: { db2 = openDB( , ); transaction = db2.transaction([ , ], ); superCat = transaction.objectStore( ).get( ); transaction.objectStore( ).delete( ); transaction.objectStore( ).add(superCat); db2.close(); } // demo7: move supercat: 2 operations in 1 transaction: export async ( ) function demo7 const await 'db2' 1 // open a new transaction, declare which stores are involved: let 'store3' 'store4' 'readwrite' // do multiple things inside the transaction, if one fails all fail: let await 'store3' 'cat001' 'store3' 'cat001' 'store4' A few things about the syntax: You first open a transaction with , and declare which stores are involved in this transaction. Notice the second argument , which means this transaction has permission to both read and write. If all you need is read, use instead (it's also the default). db.transaction() 'readwrite' 'readonly' After the transaction is opened, you can't use any of the previous methods we showed before, because those were shortcuts that encapsulate a transaction containing only one action. Instead, you do your actions using . Arguments are the same, except the first argument (the storeName) is moved forward to . ("objectStore" is the official term for a "store") transaction.objectStore(storeName).methodName(..) .objectStore(storeName) Readonly is faster than readwrite, because each store will only perform one readwrite transaction at a time, during which the store is locked, whereas multiple readonly transactions will execute at the same time. demo8: transaction on a single store, and error handling: If your transaction only involves a single store, it can be less verbose: { db1 = openDB( , ); transaction = db1.transaction( , ); transaction.store.add( , ); transaction.store.add( , ); transaction.done .then( { .log( ); }) .catch( { .error( ); }); db1.close(); } // demo8: transaction on a single store, and error handling: export async ( ) function demo8 // we'll only operate on one store this time: const await 'db1' 1 // ↓ this is equal to db1.transaction(['store2'], 'readwrite'): let 'store2' 'readwrite' // ↓ this is equal to transaction.objectStore('store2').add(..) 'foo' 'foo' 'bar' 'bar' // monitor if the transaction was successful: => () console 'All steps succeeded, changes committed!' => () console 'Something went wrong, transaction aborted' Notice in the end we monitor the promise , which tells us whether the transaction succeeded or failed. Demo8 adds some data into store2, and you can run it twice to see one success and one fail in console log (fail because keys need to be unique). transaction.done A transaction will auto commit itself when it runs out of things to do, `transaction.done` is a nice thing to monitor, but not required. DB versioning and store creation: It's finally time to answer the burning question: what the heck is ? 1 Imagine this scenario: you launched a web app, a user visited it, so DBs and stores have been created in his browser. Later you deployed a new version of the app, and changed the structure of DBs and stores. Now you have a problem: when someone visits your app, if he's an old user with the old db schema, and the db already contains data, you would want to convert his db into the new schema, while preserving the data. To solve this problem, IndexedDB enforces a version system: each db must exist as a , in DevTools you can see db1 and db2 are both at version 1. Whenever you call openDB(), you must supply a positive integer as the version number. If this integer is greater than the existing one in the browser, you can provide a callback named , and it'll fire. If the DB doesn't exist in the browser, the user's version will be 0, so the callback will also fire. db name paired with a version number upgrade Let's run demo9: { db3 = openDB( , , { : { (oldVersion === ) upgradeDB3fromV0toV1(); { db.createObjectStore( , { : }); generate100cats().forEach( { transaction.objectStore( ).add(cat); }); } }, }); db3.close(); } { ( ).fill().map( { id = + index.toString().padStart( , ); strength = .round( .random() * ); speed = .round( .random() * ); { id, strength, speed }; }); } // demo9: very explicitly create a new db and new store export async ( ) function demo9 const await 'db3' 1 upgrade ( ) => db, oldVersion, newVersion, transaction if 0 ( ) function upgradeDB3fromV0toV1 'moreCats' keyPath 'id' => cat 'moreCats' ( ) function generate100cats return new Array 100 ( ) => item, index let 'cat' 3 '0' let Math Math 100 let Math Math 100 return Demo9 creates a new , then creates a store containing 100 cats. Check the results in DevTools, then come back to look at the syntax. db3 moreCats The upgrade callback is the only place where you can create and delete stores. The upgrade callback is a transaction itself. It's not 'readonly' or 'readwrite', but a more powerful transaction type called 'versionchange', in which you have the permission to do anything, including readwrite to any stores, as well as create/delete stores. Since it's a big transaction itself, don't use single-action transaction wrappers like db.add() inside it, use the transaction object provided as an argument for you . Now let's do demo10, where we bump the version to 2 to solve the old user issue we imagined above: { db3 = openDB( , , { : { (oldVersion) { : upgradeDB3fromV0toV1(); : upgradeDB3fromV1toV2(); ; : .error( ); } { db.createObjectStore( , { : }); generate100cats().forEach( { transaction.objectStore( ).add(cat); }); } { db.createObjectStore( ); transaction.objectStore( ).add( , ); transaction.objectStore( ).add( , ); } }, }); db3.close(); } { ( ).fill().map( { id = + index.toString().padStart( , ); strength = .round( .random() * ); speed = .round( .random() * ); { id, strength, speed }; }); } // demo10: handle both upgrade: 0->2 and 1->2 export async ( ) function demo10 const await 'db3' 2 upgrade ( ) => db, oldVersion, newVersion, transaction switch case 0 // falls through case 1 break default console 'unknown db version' ( ) function upgradeDB3fromV0toV1 'moreCats' keyPath 'id' => cat 'moreCats' ( ) function upgradeDB3fromV1toV2 'userPreference' 'userPreference' false 'useDarkMode' 'userPreference' 25 'resultsPerPage' ( ) function generate100cats return new Array 100 ( ) => item, index let 'cat' 3 '0' let Math Math 100 let Math Math 100 return In demo10, we add a new store called in db3. This will happen to old users who already have . However, if a brand new user (with ) runs demo10, both and will be added for him. userPreference db3 version 1 db3 version 0 moreCats userPreference `// falls through` means "don't break". Adding this line of comment will prevent eslint from nagging you to add a break. You can delete db3 in DevTools, then simulate an old user by clicking demo9 then demo10, and simulate a new user by directly clicking demo10. Version upgrade without schema change: Many people think of as a "schema change" event. True, a version change is the only place where you can create or delete stores, but there are often other scenarios when a version change is good choice even if you don't need to add / delete stores. upgrade In demo10, we added a store called , and set the initial value to be , which simulates some settings that the user can change. Now let's imagine you launched a new version, where you added a new preference called that defaults to 'English'; you also implemented infinite scroll, so 'resultsPerPage' is no longer needed; finally you changed 'useDarkMode` from a boolean to a string, which could be 'light' | 'dark' | 'automatic'. How do you change the initial settings for new users, while preserving the saved preferences of old users? userPreference 'useDarkMode': false, 'resultsPerPage': 25 language It's a common problem that web developers face. When you're storing user preferences in localStorage, you might use a package like . Here with IndexedDB, let's solve it in demo11 with a version change: left-merge demo11: upgrade db version even when no schema change is needed: { db3 = openDB( , , { : (db, oldVersion, newVersion, transaction) => { (oldVersion) { : upgradeDB3fromV0toV1(); : upgradeDB3fromV1toV2(); : upgradeDB3fromV2toV3(); ; : .error( ); } { db.createObjectStore( , { : }); generate100cats().forEach( { transaction.objectStore( ).add(cat); }); } { db.createObjectStore( ); transaction.objectStore( ).add( , ); transaction.objectStore( ).add( , ); } { store = transaction.objectStore( ); store.put( , ); store.delete( ); colorTheme = ; useDarkMode = store.get( ); (oldVersion === && useDarkMode === ) colorTheme = ; (oldVersion === && useDarkMode === ) colorTheme = ; store.put(colorTheme, ); store.delete( ); } }, }); db3.close(); } { ( ).fill().map( { id = + index.toString().padStart( , ); strength = .round( .random() * ); speed = .round( .random() * ); { id, strength, speed }; }); } // demo11: upgrade db version even when no schema change is needed: export async ( ) function demo11 const await 'db3' 3 upgrade async switch case 0 // falls through case 1 // falls through case 2 await break default console 'unknown db version' ( ) function upgradeDB3fromV0toV1 'moreCats' keyPath 'id' => cat 'moreCats' ( ) function upgradeDB3fromV1toV2 'userPreference' 'userPreference' false 'useDarkMode' 'userPreference' 25 'resultsPerPage' async ( ) function upgradeDB3fromV2toV3 const 'userPreference' 'English' 'language' 'resultsPerPage' let 'automatic' let await 'useDarkMode' if 2 false 'light' if 2 true 'dark' 'colorTheme' 'useDarkMode' ( ) function generate100cats return new Array 100 ( ) => item, index let 'cat' 3 '0' let Math Math 10 let Math Math 10 return We didn't add or delete any stores here, so it could've been done without a version change, but a version change makes it much more organized and less error prone. You can simulate all possible scenarios by deleting db3 in DevTools, then click demo , or , or , or just . 9 10 11 9 11 10 11 11 Where to write your "upgrade" callback? If you establish multiple connections to the same db in your code, you'd want to fire the version change on app start, before any db connections are established. Then when you later call openDB(), you can omit the callback in the 3rd argument. upgrade If you reuse a single connection with the pattern mentioned in between demo3 and demo4, then you can just provide the callback there. Remember it only fires when the db version in user's browser is lower than the version in openDB(). upgrade The block() and blocking() callback: Similar to localStorage, IndexedDB uses the , so if user opens your app twice in two tabs, they'd access the same db. It's usually not an issue, but imagine if the user opens your app, then you pushed out a version upgrade, then the user opened the 2nd tab. Now you have a problem: the same db can't have 2 versions at the same time in 2 tabs. same origin policy To solve this issue, there are another 2 callbacks that you may provide to a db connection besides , they're called and : upgrade blocked blocking db = openDB(dbName, version, { : { .log( ); }, : { }, : { .log( ); } }); const await blocked => () // seems an older version of this app is running in another tab console `Please close this app opened in other browser tabs.` upgrade ( ) => db, oldVersion, newVersion, transaction // … blocking => () // seems the user just opened this app again in a new tab // which happens to have gotten a version change console `App is outdated, please close this tab` When the 2-tab-problem happens, the callback fires in the old openDB() connection which prevented from firing, and fires in the new connection, will not fire in the new connection until db.close() is called on the old connection or the old tab is closed. blocking upgrade blocked upgrade If you think it's a pain in the ass to have to worry about this kind of scenarios, I 100% agree! Luckily there's a better way: use a service worker and precache your js files, so that no matter how many tabs the user opens, they'll all use the same js files until all tabs are closed, which means same db version across all tabs. However, that's a totally different topic for another day. Indexing: You can create indexes (indices?) on a store. I don't care how you understand indexes in other databases, but in IndexedDB, an is a duplicated copy of your store, only sorted in different order. You can think of it as a "shadow store" based off of the real store, and the two are always in sync. Let's see what it looks like: index demo12: create an index on the 100 cats' strength: { db3 = openDB( , , { : { store = transaction.objectStore( ); store.createIndex( , ); }, }); db3.close(); } // demo12: create an index on the 100 cats' strength: export async ( ) function demo12 const await 'db3' 4 upgrade ( ) => db, oldVersion, newVersion, transaction // upgrade to v4 in a less careful manner: const 'moreCats' 'strengthIndex' 'strength' Run demo12, then check DevTools, you'll see the "shadow store" named has appeared under . Note a few things: strengthIndex moreCats 1. The event is where you can add an index, so we had to upgrade db3 to version . upgrade the only place 4 2. This upgrade didn't follow the 0->1, 1->2, 2->3 pattern. What we want to show here is there's no fixed rule of how to do a version upgrade, you can do whatever you see fit. However, this one will crash if you delete db3 then directly click demo12, which simulates a bug that would happen to brand new users who directly land on v4 (these users didn't have the store, so you can't create the index). 3. In DevTools, you can see the store has the same 100 cats as the main store, only the keys are different - that's exactly what an index is: a store with same values but different keys. Now you can retrieve values from it by using the new keys, but you can't make changes to it because it's just a shadow. The shadow auto changes when the main store changes. strengthIndex Adding an index is like creating the same store with a different 'keyPath'. Just like how the main store is constantly sorted by the main key, the index store is auto sorted by its own key. Now let's retrieve values from the index: demo13: get values from index by index key: { db3 = openDB( , ); transaction = db3.transaction( ); strengthIndex = transaction.store.index( ); strongestCats = strengthIndex.getAll( ); .log( , strongestCats); oneStrongCat = strengthIndex.get( ); .log( , oneStrongCat); db3.close(); } // demo13: get values from index by key export async ( ) function demo13 const await 'db3' 4 const 'moreCats' const 'strengthIndex' // get all entries where the key is 10: let await 10 console 'strongest cats: ' // get the first entry where the key is 10: let await 10 console 'a strong cat: ' Run demo13 and check the results in console logs. As you'll notice, since became key, the key is no longer unique, so will only get the first match. To get all matches, we use . strength .get() .getAll() Demo13 performed two queries in one transaction. You can also use the single-action transaction shortcuts so you don't need to write transactions yourself, they're called and , again, intellisense will help you. db.getFromIndex() db.getAllFromIndex() demo14: get values from index by key using shortcuts: { db3 = openDB( , ); weakestCats = db3.getAllFromIndex( , , ); .log( , weakestCats); oneWeakCat = db3.getFromIndex( , , ); .log( , oneWeakCat); db3.close(); } // demo14: get values from index by key using shortcuts: export async ( ) function demo14 const await 'db3' 4 // do similar things as demo13, but use single-action transaction shortcuts: let await 'moreCats' 'strengthIndex' 0 console 'weakest cats: ' let await 'moreCats' 'strengthIndex' 0 console 'a weak cat: ' Demo14 retrieved value from in two transactions. strengthIndex Simple search by range: It's a very common task in all databases to search for items that satisfy a certain criteria, for instance you may want to find "all cats with a strength greater than 7". With idb, we can do it still by using , but give a in place of keys. getAll() range The is constructed by calling a native browser API called : Range Object IDBKeyRange demo15: find items matching a condition by using range: { db3 = openDB( , ); strongRange = IDBKeyRange.lowerBound( ); midRange = IDBKeyRange.bound( , ); weakRange = IDBKeyRange.upperBound( ); [strongCats, ordinaryCats, weakCats] = [ db3.getAllFromIndex( , , strongRange), db3.getAllFromIndex( , , midRange), db3.getAllFromIndex( , , weakRange), ]; .log( , strongCats); .log( , ordinaryCats); .log( , weakCats); db3.close(); } // demo15: find items matching a condition by using range export async ( ) function demo15 const await 'db3' 4 // create some ranges. note that IDBKeyRange is a native browser API, // it's not imported from idb, just use it: const 8 const 3 7 const 2 let await 'moreCats' 'strengthIndex' await 'moreCats' 'strengthIndex' await 'moreCats' 'strengthIndex' console 'strong cats (strength >= 8): ' console 'ordinary cats (strength from 3 to 7): ' console 'weak cats (strength <=2): ' Run demo15 and you'll see how we separated the 100 cats into 3 tiers. Whenever you call .get() or .getAll() with idb, you can always substitute the key with a range, whether that's a primary key or index key. Range works on strings too, since strings can be key, and keys are auto sorted. You may do something like For all the ways to create different ranges, check . IDBKeyRange.bound('cat042', 'cat077'). MDN Looping and complicated searching with cursor: IndexedDB doesn't provide a declarative language like SQL for us to find things ("declarative" means "find xxx for me, and I don't care what algorithm you use, just give me the results"), so a lot of times you have to do it yourself with JavaScript, writing loops and stuff. You might've been thinking all along: "Yes! Why do I have to learn how to the database, why can't I just , then find what I want with JavaScript?" query getAll() Indeed you can, but there's one problem: IndexedDB is designed to be a database, which means some people may store a million records in it. If you getAll(), you need to first read a million records into memory, then loop over it. To avoid using too much memory, IndexedDB provides a tool called a , which is used to loop over a store directly. A cursor is like a pointer that points to a position in a store, you can read the record at that position, then advance the position by 1, then read the next record, and so on. Let's take a look: cursor demo16: loop over the store with a cursor: { db3 = openDB( , ); store = db3.transaction( ).store; cursor = store.openCursor(); .log( , cursor.key); .log( , cursor.value); cursor = cursor.continue(); .log( , cursor.key); .log( , cursor.value); ( ) { { strength, speed } = cursor.value; (strength >= && speed >= ) { .log( , cursor.value); } cursor = cursor.continue(); (!cursor) ; } db3.close(); } // demo16: loop over the store with a cursor export async ( ) function demo16 const await 'db3' 4 // open a 'readonly' transaction: let 'moreCats' // create a cursor, inspect where it's pointing at: let await console 'cursor.key: ' console 'cursor.value: ' // move to next position: await // inspect the new position: console 'cursor.key: ' console 'cursor.value: ' // keep moving until the end of the store // look for cats with strength and speed both greater than 8 while true const if 8 8 console 'found a good cat! ' await if break Check console log and you'll see it's pretty straight forward: you create a cursor, which starts at position 0, then you move it one position at time by calling , and read data from and . .continue() cursor.key cursor.value You can also use a cursor on an index, and / or with a range: demo17: use cursor on a range and/or on an index: { db3 = openDB( , ); store = db3.transaction( ).store; range = IDBKeyRange.bound( , ); cursor1 = store.openCursor(range); ( ) { .log( , cursor1.key); cursor1 = cursor1.continue(); (!cursor1) ; } .log( ); index = db3.transaction( ).store.index( ); cursor2 = index.openCursor(); .log( , cursor2.key); .log( , cursor2.primaryKey); .log( , cursor2.value); db3.close(); } // demo17: use cursor on a range and/or on an index export async ( ) function demo17 const await 'db3' 4 let 'moreCats' // create a cursor on a very small range: const 'cat042' 'cat045' let await // loop over the range: while true console 'cursor1.key: ' await if break console '------------' // create a cursor on an index: let 'moreCats' 'strengthIndex' let await // cursor.key will be the key of the index: console 'cursor2.key:' // the primary key will be located in cursor.primaryKey: console 'cursor2.primaryKey:' // it's the first item in the index, so it's a cat with strength 0 console 'cursor2.value:' As you see, when on a index, becomes the index key, and the primary key can be found in . cursor.key cursor.primaryKey Use with Typescript: If you use Typescript, don't forget to , which makes your life so much better. type your store Use in web workers / service workers: Unlike localStorage, you use IndexedDB in service workers. It's a good way to store states in your worker (worker should be stateless because it can be killed any time), and it's a good way to pass data between the worker and your app, and it's very well suited for PWAs, because IndexedDB is designed to store lots of data for offline apps. can Most people write their service worker with , in an environment where they can import npm packages. However, if module imports isn't possible in your setup, you can import into your serviceWorker . workbox idb this way I used it when creating a , and it helped me so much. Hope it'll help you too. That's all for IndexedDB with idb. desktop app for Google Tasks