Skip to main content

IndexedDB: Database inside browser

Written by Ayush Chugh, Full Stack Developer. This article has 961 words and covers topics in web development and software engineering.

Published on May 19, 2025 (9 months ago)

IndexedDB: Database inside browser

IndexedDB is a low-level API for client-side storage of significant amounts of structured data, including files/blobs. Unlike the simple key-value model of localStorage, it lets you store and query objects via indexes, run transactions, and work with large volumes of data—making it ideal for offline-first web apps, progressive web apps, and anywhere you need robust local persistence.

Why IndexedDB?

  • Large storage quotas: Browsers typically allow hundreds of megabytes (or even more) per origin, versus the 5 MB limit of localStorage.
  • Structured objects: Store complex JavaScript objects directly—no need to stringify.
  • Indexes & queries: Define secondary indexes for efficient lookups by object properties.
  • Transactions: Atomic reads and writes across multiple object stores.
  • Asynchronous API: Prevents UI blocking, especially important for large datasets.

Core Concepts

  1. Database: A logical container identified by name and version.
  2. Object Store: Like a table in SQL, holds records (JavaScript objects).
  3. Key & Key Path: Each record has a primary key. You can let IndexedDB auto-generate it or specify a key path (a property name).
  4. Index: Secondary lookup on object properties. Allows queries like “find all users by email.”
  5. Transaction: Defines scope for reading/writing. Transactions can be read-only or read-write across one or more object stores; they either fully succeed or roll back.
  6. Cursor: Iterates over records in an object store or index.

Opening a Database

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

request.onupgradeneeded = event => {
  const db = event.target.result;
  // Create an object store named "contacts" with auto-incremented keys
  const store = db.createObjectStore('contacts', { keyPath: 'id', autoIncrement: true });
  // Create an index on the "email" property
  store.createIndex('by_email', 'email', { unique: true });
};

request.onsuccess = event => {
  const db = event.target.result;
  // db is now ready for operations
};

request.onerror = event => {
  console.error('Database error:', event.target.error);
};

CRUD Operations

Adding or Updating Records

function saveContact(db, contact) {
  const tx = db.transaction('contacts', 'readwrite');
  const store = tx.objectStore('contacts');
  // .put() will insert or update based on the keyPath
  store.put(contact);
  return tx.complete;
}

// Usage:
request.onsuccess = e => {
  const db = e.target.result;
  saveContact(db, { name: 'Alice', email: 'alice@example.com' })
    .then(() => console.log('Saved successfully'))
    .catch(err => console.error(err));
};

Reading Records

function getContactByEmail(db, email) {
  const tx = db.transaction('contacts', 'readonly');
  const index = tx.objectStore('contacts').index('by_email');
  return index.get(email); // returns a request; wrap in Promise if you like
}

// Usage:
getContactByEmail(db, 'alice@example.com')
  .onsuccess = e => console.log('Contact:', e.target.result);

Deleting Records

function deleteContact(db, id) {
  const tx = db.transaction('contacts', 'readwrite');
  tx.objectStore('contacts').delete(id);
  return tx.complete;
}

Example: Viewing and Deleting Contacts

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>IndexedDB Contacts Example</title>
  <style>
    table { border-collapse: collapse; width: 100%; margin-top: 1em; }
    th, td { border: 1px solid #ccc; padding: 0.5em; text-align: left; }
    button { padding: 0.25em 0.5em; }
  </style>
</head>
<body>
  <h1>IndexedDB Contacts</h1>

  <form id="contactForm">
    <label for="name">Name</label>
    <input id="name" type="text" placeholder="Name" required />

    <label for="email">Email</label>
    <input id="email" type="email" placeholder="Email" required />

    <button type="submit">Save</button>
  </form>

  <h2>All Contacts</h2>
  <table id="contactsTable">
    <thead>
      <tr>
        <th>ID</th><th>Name</th><th>Email</th><th>Actions</th>
      </tr>
    </thead>
    <tbody><!-- rows injected here --></tbody>
  </table>

  <script>
    function openDB() {
      return new Promise((resolve, reject) => {
        const request = indexedDB.open("myDB", 2);
        request.onupgradeneeded = () => {
          const db = request.result;
          if (!db.objectStoreNames.contains("contacts")) {
            const store = db.createObjectStore("contacts", {
              keyPath: "id", autoIncrement: true
            });
            store.createIndex("by_email", "email", { unique: true });
          }
        };
        request.onsuccess = () => resolve(request.result);
        request.onerror   = () => reject(request.error);
      });
    }

    function saveContact(db, contact) {
      return new Promise((resolve, reject) => {
        const tx    = db.transaction("contacts", "readwrite");
        const store = tx.objectStore("contacts");
        const req   = store.put(contact);
        req.onsuccess = () => resolve(req.result);
        req.onerror   = () => reject(req.error);
      });
    }

    function getAllContacts(db) {
      return new Promise((resolve, reject) => {
        const tx    = db.transaction("contacts", "readonly");
        const store = tx.objectStore("contacts");
        const req   = store.getAll();
        req.onsuccess = () => resolve(req.result);
        req.onerror   = () => reject(req.error);
      });
    }

    function deleteContact(db, id) {
      return new Promise((resolve, reject) => {
        const tx    = db.transaction("contacts", "readwrite");
        const store = tx.objectStore("contacts");
        const req   = store.delete(id);
        req.onsuccess = () => resolve();
        req.onerror   = () => reject(req.error);
      });
    }

    async function renderContacts() {
      const db       = await openDB();
      const contacts = await getAllContacts(db);
      const tbody    = document.querySelector("#contactsTable tbody");
      tbody.innerHTML = "";
      contacts.forEach(contact => {
        const tr = document.createElement("tr");
        tr.innerHTML = `
          <td>${contact.id}</td>
          <td>${contact.name}</td>
          <td>${contact.email}</td>
          <td><button data-id="${contact.id}">Delete</button></td>
        `;
        tbody.appendChild(tr);
      });
      tbody.querySelectorAll("button").forEach(btn => {
        btn.addEventListener("click", async () => {
          const id = Number(btn.dataset.id);
          await deleteContact(await openDB(), id);
          renderContacts();
        });
      });
    }

    document.getElementById("contactForm")
      .addEventListener("submit", async e => {
        e.preventDefault();
        const name  = e.target.name.value.trim();
        const email = e.target.email.value.trim();
        if (!name || !email) return;
        const db = await openDB();
        await saveContact(db, { name, email });
        e.target.reset();
        renderContacts();
      });

    // initial render
    renderContacts();
  </script>
</body>
</html>

Output

output of above codebase

Best Practices

  • Versioning & Migrations: Handle schema upgrades carefully in onupgradeneeded.
  • Error Handling: Always set onerror on requests and transactions.
  • Transactions Scope: Keep transactions short; long-running ones can time out.
  • Feature Detection: Fallback to localStorage or in-memory if IndexedDB isn’t available:
if (!window.indexedDB) {
  console.warn("IndexedDB not supported — falling back.");
  // fallback logic here
}
  • Cleanup: Implement logic to purge stale data or compact large stores.
  • Security: Never store sensitive data unencrypted.

Real-World Use Cases

  • Offline-first Apps: Cache API responses and serve from IndexedDB when offline.
  • Large Media Storage: Store images, audio, or video blobs.
  • Form Drafts: Auto-save user inputs in progress.
  • Analytics & Logs: Buffer log events locally before batching to server.

Conclusion

IndexedDB unlocks powerful client-side storage capabilities that go far beyond simple key-value stores. Whether you’re building a PWA that works offline, storing user drafts, or caching media, mastering its concepts—databases, object stores, indexes, and transactions—will help you create resilient, high-performance web applications.