Your browser can store data locally. But localStorage — the simple tool everyone uses — is basically a sticky note: tiny (5MB), can only hold text, and freezes your app while reading/writing. IndexedDB is the upgrade: a real database in the browser, holds gigabytes of any data, runs in the background without freezing anything, and lets you search by field. The catch? Its raw API is painful to write. So most devs use a library on top of it.
Imagine you're building a web app — like a todo list, a notes app, or an e-commerce site. You want to remember data between page refreshes. You have a few options… but most of them kinda suck for anything beyond the simplest case.
Think of your browser like a desk at work. localStorage is a sticky note stuck to your monitor — fast to write, easy to read, but tiny and only holds plain text. IndexedDB is a proper filing cabinet beside your desk — way more space, organized folders, you can search through it, and it doesn't interrupt what you're doing while you file something.
IndexedDB is a NoSQL database built into your browser. Every modern browser ships it — Chrome, Firefox, Safari, Edge. No install needed. No backend. It lives on the user's device.
It's called "Indexed" because you can create indexes on your data — like telling the database "let me quickly find users by email" — without scanning every record.
Think of IndexedDB like a library. The library is your database. Different genres (Fiction, Science, History) are your object stores — like tables. The catalog system that lets you find books by author or title is the index. When you checkout or return books, a transaction makes sure you're counted properly — no half-checkouts. The librarian works while you browse — async, nothing blocks.
The raw API uses an event-based callback pattern (old-school JavaScript, before Promises were invented). It's verbose. Most devs wrap it with a library. But understanding the flow helps you debug issues.
// 1. Open (or create) a database named "MyApp" at version 1 const request = indexedDB.open("MyApp", 1); // 2. This runs when db is FIRST created (or version bumped) request.onupgradeneeded = (event) => { const db = event.target.result; // Create an object store called "users" with auto-increment keys const store = db.createObjectStore("users", { keyPath: "id" }); // Add an index so we can query by email fast store.createIndex("by_email", "email", { unique: true }); }; // 3. Success — db is ready request.onsuccess = (event) => { const db = event.target.result; // Now you can read/write... };
// npm install dexie import Dexie from "dexie"; // Define your database schema const db = new Dexie("MyApp"); db.version(1).stores({ users: "id, email, name" // id = primary key, email+name = indexed }); // Write a user — clean async/await await db.users.add({ id: 1, email: "ali@example.com", name: "Ali" }); // Query by index — just like an ORM const user = await db.users.where("email").equals("ali@example.com").first(); console.log(user.name); // "Ali"
Dexie.js wraps all the callback madness and gives you a clean promise-based API. Under the hood it's still IndexedDB — same storage, same limits, same browser support.
Before picking IndexedDB, understand what else exists. Each tool has a specific job.
| Storage | Size | Data types | Blocking? | Persists? | Searchable? |
|---|---|---|---|---|---|
| Cookies | 4 KB | String only | Yes | Configurable | No |
| localStorage | 5–10 MB | String only | Yes! | Yes | No |
| sessionStorage | 5–10 MB | String only | Yes! | Tab only | No |
| Cache API | ~50 MB+ | HTTP responses | No | Yes | No |
| IndexedDB | 50 MB – GBs | Any JS object | No | Yes | Yes |
IndexedDB isn't always the right tool. Here's the honest breakdown:
Need to talk to server? → Cookies or API calls
Just a tiny setting (<100KB, string)? → localStorage
Gone when tab closes? → sessionStorage
Caching whole network responses? → Cache API
Large data, offline support, or real queries? → IndexedDB
Here's what a real-world pattern looks like in React using Dexie.js — the most popular IndexedDB wrapper. This pattern is used by apps like Notion, Linear, and many offline-first tools.
import Dexie from "dexie"; // Think of this like defining your tables and columns export const db = new Dexie("TodoApp"); db.version(1).stores({ // "++id" = auto-increment primary key // other fields listed = these get indexes (fast search) todos: "++id, title, done, createdAt" });
import { useLiveQuery } from "dexie-react-hooks"; import { db } from "./db"; function TodoList() { // useLiveQuery = re-renders automatically when DB changes const todos = useLiveQuery(() => db.todos.orderBy("createdAt").toArray() ); const addTodo = async (title) => { await db.todos.add({ title, done: false, createdAt: Date.now() }); // No setState needed — useLiveQuery re-renders automatically! }; const toggleDone = async (id, currentDone) => { await db.todos.update(id, { done: !currentDone }); }; return ( <ul> {todos?.map(todo => ( <li key={todo.id} onClick={() => toggleDone(todo.id, todo.done)}> {todo.title} </li> ))} </ul> ); }
useLiveQuery subscribes to the database, any component that queries the same data automatically re-renders when another component writes to it — even across browser tabs. This is way harder to build with localStorage.
Dexie.js — Most popular, clean API, React hooks support
idb — Google's thin Promise wrapper, minimal overhead
RxDB — Full reactive database, best for complex sync
PouchDB — CouchDB-compatible, built-in server sync
Safari limits — iOS Safari may clear IndexedDB when disk is low
Schema migrations — bumping db version = onupgradeneeded must handle old data
Not encrypted — data visible to other JS on the page (XSS risk)
Not shared across origins — localhost:3000 ≠ localhost:3001
IndexedDB is the raw browser API — built into every browser, low-level, callback-based. Like talking to a database with assembly language. It works, but writing it directly is painful.
RxDB is a full library built on top of IndexedDB. It adds things the raw API doesn't have:
RxDB is overkill for most simple apps. Dexie.js is the sweet spot — thin wrapper, clean API, no heavy abstractions.
| Tool | Best for |
|---|---|
| Dexie.js | Most common. Clean API, IndexedDB wrapper, React hooks included |
| PouchDB | Need CouchDB-style server sync. Offline-first with replication |
| idb | Google's minimal wrapper. Tiny, no magic, just Promises |
| RxDB | Complex reactive apps, multi-device sync needed |
| SQLite via WASM | Heavy relational queries, fastest option (newer, more complex setup) |
| localStorage | Tiny data <5MB, simple key-value, no search needed |
Catatan keuangan (single device, basic): depends on complexity. Just storing transactions → localStorage works fine under ~500 entries. Want to filter by date, sum by category, search by merchant → Dexie.js is worth it. 10KB overhead, much better querying.
Padel Americano/Mexicano with match history: Yes, good fit. Tournament records, player rankings, match results — structured data that grows over time and needs querying ("show last 10 games", "sort by win rate"). No server needed = IndexedDB is the natural choice.
Both apps are local-first by design — user owns their data, no backend required. This is exactly the use case IndexedDB was built for. Add an Export JSON button so users can back up or move their data.
Data lives in that browser, on that device, for that origin (domain). Nowhere else.
localhost:3000 vs localhost:3001: different storage — origin-scopedSolution: build an Export / Import JSON feature. Let users download their data as a file and re-import it in a different browser or device. Simple, zero backend needed.
No — IndexedDB data sits on disk and is not auto-loaded into memory when your app starts. 10,000 records in the DB doesn't mean 10,000 records loaded on page open. You fetch only what you query, when you query it.
Performance degrades only if you:
For padel/finance app scale (hundreds to low thousands of records) — zero perceptible impact. Add indexes on fields you filter/sort by (date, category, player) and you're fine.
Your padel app is exactly this category — local-first, persistent, no server required. Good fit.