IndexedDB ยท Astro Tech Blog

IndexedDB

IndexedDB is a full client-side database built into the browser. It stores structured data (objects), supports indexes for fast lookups, and handles large amounts of data (hundreds of MB/GB).

Key Concepts

ConceptDescription
DatabaseOne per origin (e.g., myApp)
Object StoreLike a table โ€” holds records
IndexLike a SQL index โ€” fast lookups by a property
KeyUnique identifier for each record
TransactionAtomic read/write operations
CursorIterate over multiple records

Opening a Database

const request = indexedDB.open('MyApp', 1);

request.onupgradeneeded = function(event) {
  const db = event.target.result;
  // Create object stores here (only on version upgrade)
};

request.onsuccess = function(event) {
  const db = event.target.result;
  // Use the database
};

request.onerror = function(event) {
  console.error('Database error:', event.target.error);
};
Demo: Open IndexedDB Database
HTML
<div>
<button id='idb-open'>Open/Create Database</button>
<pre id='idb-out' style='background:#f1f5f9;padding:12px;border-radius:6px;'></pre>
</div>
JavaScript
const out = document.getElementById('idb-out');
let db = null;

document.getElementById('idb-open').onclick = function() {
const request = indexedDB.open('DemoDB', 1);

request.onupgradeneeded = function(e) {
const database = e.target.result;
out.textContent = 'onupgradeneeded: Creating object stores...\\n';

if (!database.objectStoreNames.contains('notes')) {
const store = database.createObjectStore('notes', { keyPath: 'id', autoIncrement: true });
store.createIndex('title', 'title', { unique: false });
store.createIndex('created', 'created', { unique: false });
out.textContent += '  Created 'notes' store with indexes\\n';
}
};

request.onsuccess = function(e) {
db = e.target.result;
out.textContent += 'โœ… Database opened! Version: ' + db.version + '\\n';
out.textContent += '  Object stores: ' + Array.from(db.objectStoreNames).join(', ');
this.disabled = true;
this.textContent = 'Database Ready';
};

request.onerror = function() {
out.textContent = 'โŒ Error opening database';
};
};
Live Output Window

Adding Data

const transaction = db.transaction('notes', 'readwrite');
const store = transaction.objectStore('notes');

const note = { title: 'My Note', body: 'Hello IndexedDB!', created: new Date() };
const request = store.add(note);

request.onsuccess = function() {
  console.log('Added with id:', request.result);
};
Demo: Adding Data to IndexedDB
HTML
<div>
<button id='idb-open2' disabled>1. Open DB First</button>
<button id='idb-add' disabled>2. Add Record</button>
<pre id='idb-add-out' style='background:#f1f5f9;padding:12px;border-radius:6px;'></pre>
</div>
JavaScript
const out = document.getElementById('idb-add-out');
let db = null;

const openBtn = document.getElementById('idb-open2');
const addBtn = document.getElementById('idb-add');

openBtn.onclick = function() {
const req = indexedDB.open('NotesDB', 1);

req.onupgradeneeded = function(e) {
const d = e.target.result;
if (!d.objectStoreNames.contains('notes')) {
d.createObjectStore('notes', { keyPath: 'id', autoIncrement: true });
}
};

req.onsuccess = function(e) {
db = e.target.result;
out.textContent = 'โœ… Database ready';
openBtn.disabled = true;
openBtn.textContent = 'Ready';
addBtn.disabled = false;
};

req.onerror = function() {
out.textContent = 'โŒ Error opening';
};
};

addBtn.onclick = function() {
if (!db) return;

const note = {
title: 'Note ' + (Math.random() * 1000).toFixed(0),
body: 'Created at ' + new Date().toLocaleTimeString(),
created: new Date()
};

const tx = db.transaction('notes', 'readwrite');
const store = tx.objectStore('notes');
const req = store.add(note);

req.onsuccess = function() {
out.textContent = 'โœ… Added: id=' + req.result + ', title=' + note.title + '';
};

req.onerror = function() {
out.textContent = 'โŒ Add failed: ' + req.error;
};
};
Live Output Window

Reading Data

// Get by key
const request = store.get(1);
request.onsuccess = function() {
  console.log(request.result);
};

// Get all
const request = store.getAll();
request.onsuccess = function() {
  console.log(request.result); // array
};

Using Indexes

// Get by index (title)
const index = store.index('title');
const request = index.get('My Note');

// Cursor โ€” iterate all records
const cursor = store.openCursor();
cursor.onsuccess = function(event) {
  const cursor = event.target.result;
  if (cursor) {
    console.log(cursor.key, cursor.value);
    cursor.continue(); // next
  }
};

IndexedDB vs localStorage

FeatureIndexedDBlocalStorage
Data typeObjects, Blobs, FilesStrings only
Indexesโœ… Multiple indexesโŒ None
CapacityHundreds of MB+~5-10MB
Asyncโœ… (transactions)โŒ (sync)
Complex queriesโœ… (cursors, ranges)โŒ None
ComplexityHigherVery simple

Transaction Modes

ModeDescription
readonlyCan only read (multiple can run in parallel)
readwriteCan read and write (one at a time)
versionchangeSchema changes (onupgradeneeded)

Key Range Queries

// All records with key >= 5 and <= 10
const range = IDBKeyRange.bound(5, 10);
store.openCursor(range);

// All records with key > 5
const range = IDBKeyRange.lowerBound(5, true);

// All records with key < 10
const range = IDBKeyRange.upperBound(10, true);

// Single key
const range = IDBKeyRange.only(5);

Practical: Note Taking App

IndexedDB is ideal for offline-first apps, draft saving, and client-side caches. The pattern:

  1. Open database on app start
  2. On create/update/delete, write to IndexedDB
  3. On read, query IndexedDB
  4. Sync with server in the background

Key Takeaways

  • IndexedDB stores structured objects with indexes for fast lookups
  • Always create/modify object stores in onupgradeneeded
  • Use transactions (readonly / readwrite) for all operations
  • Keys can be auto-incrementing or a property you define
  • Indexes enable fast lookups by non-key properties
  • Cursors iterate over matching records
  • Much more powerful than localStorage for complex data
  • Async API โ€” uses onsuccess/onerror callbacks (or wrap in Promises)