IndexedDB Storage Adapter

The IndexedDBAdapter extends the abstract StorageAdapter class, providing a concrete implementation for persisting data to the browser's IndexedDB. This adapter offers persistent, high-capacity client-side storage with enhanced capabilities including encryption, expiration management, metadata tracking, and optional cross-tab synchronization.

1. Getting Started

Initialize the adapter with the desired options:

        
import IndexedDBAdapter from "storagefy";

const adapter = new IndexedDBAdapter({
  dbName: "my-app",               // required - database name
  storeName: "my-store",          // optional - defaults to `${dbName}_store`
  encrypt: true,                  // optional, default false
  version: 1,                     // optional, default 1
  expireCheckInterval: 2000,      // optional, default 1000ms
  description: "App database",    // optional
  channelName: "idb-channel"      // optional - used for sync between tabs
  enableSyncTabs: false,          // optional, default false - Whether to enable sync 
                                  // automatically on change key value
});
        
      

The channelName enables automatic synchronization of data across open tabs using the CrossTabChannel communication system. This is particularly useful for applications where you want to maintain state consistency across multiple tabs.

2. Core Concepts

Persistent Storage

IndexedDB provides a robust, persistent storage mechanism that can handle significant amounts of structured data. Unlike localStorage or sessionStorage, IndexedDB can store much larger data volumes (typically limited only by disk space) and supports complex data structures.

Database Organization

Data is stored in an object store within the IndexedDB database. Each adapter instance creates or uses a database with the specified dbName and an object store (default name is {dbName}_store). All keys are automatically namespaced using the format {dbName}__ to maintain organization.

Encryption

When the encrypt: true option is set, all values are encrypted before storage and decrypted when retrieved. Keys are also obfuscated to enhance security, providing an additional layer of protection for sensitive data.

Asynchronous Operations

All IndexedDB operations are inherently asynchronous. The adapter handles this complexity internally, providing a clean Promise-based API for all operations that makes asynchronous storage operations straightforward to work with.

Cross-Tab Communication

When a channelName is provided, changes to data are broadcast across browser tabs. This enables real-time synchronization of state across multiple open instances of your application.

Expiration Management

The adapter includes a built-in expiration system that automatically removes expired items at regular intervals (configurable via expireCheckInterval). This helps manage temporary data without manual cleanup while maintaining long-term data integrity.

Database Versioning

IndexedDB supports versioning to manage schema changes over time. The adapter handles version upgrades automatically when you specify a new version number, ensuring backward compatibility with previously stored data.

3. Test It

IndexedDB Content:

 

4. API Reference

await adapter.get(key)

Retrieves a value. Returns null if not found or expired. Handles decryption automatically if encryption is enabled.

          
await adapter.set("userId", "user123");
const value = await adapter.get("userId"); // "user123"
          
        

await adapter.set(key, value, expire?)

Stores a value. Optional expire is in milliseconds from now. Handles encryption automatically if enabled and broadcasts changes to other tabs if using a channel.

          
// Save user data that expires in 7 days
await adapter.set("userData", userData, 7 * 24 * 60 * 60 * 1000);
          
        

await adapter.delete(key)

Removes a key and its expiration metadata. Also broadcasts deletion to other tabs if using a channel.

          await adapter.delete("userId");
        

await adapter.list(prefix?)

Returns all keys/values matching the prefix (defaults to all in namespace). Decrypts values if encryption is enabled.

          
await adapter.set("user.name", "John");
await adapter.set("user.email", "john@example.com");
const userData = await adapter.list("user."); 
// [{ key: "user.name", value: "John" }, { key: "user.email", value: "john@example.com" }]
          
        

await adapter.has(key)

Returns true if the key exists in the IndexedDB store and has not expired.

await adapter.has("userId"); // true or false

await adapter.clear()

Clears all data, including metadata and expirations, within this adapter's namespace.

await adapter.clear();

await adapter.reset()

Removes only data keys related to the namespace, not metadata. Useful for clearing user data while preserving system configuration.

await adapter.reset();

await adapter.setExpire(key, timestamp)

Set a custom expiration time (timestamp in ms) for a specific key.

          
const futureDate = Date.now() + 30 * 24 * 60 * 60 * 1000; // 30 days
await adapter.setExpire("userData", futureDate);
          
        

await adapter.getExpire(key)

Returns the expiration timestamp for a key, or null if no expiration is set.

          
const expireTime = await adapter.getExpire("userData");
// Returns timestamp or null
          
        

await adapter.deleteExpire(key)

Removes the expiration for the given key, making it persist indefinitely.

          
await adapter.deleteExpire("userData");
          
        

await adapter.clearExpire()

Deletes all keys that have expired based on their timestamps.

          
await adapter.clearExpire();
          
        

await adapter.waitReadiness(timeout?, tries?)

Waits for the IndexedDB to be ready before proceeding. Useful when executing operations immediately after initialization.

          
await adapter.waitReadiness();
// Now safe to perform operations
          
        

adapter.destroy()

Cleans up resources, stops the expiration timer, and removes event listeners. Called automatically on page unload.

          
adapter.destroy(); // Clean up before component unmount
          
        

adapter.emitDataChange(key, value, origin)

Explicitly broadcasts a data change event to other tabs (usually called automatically by set/delete).

          
adapter.emitDataChange("appState", newState, "tab-123");
          
        

adapter.onDataChanged(callback)

Registers a callback function to handle data changes from other tabs or contexts.

          
adapter.onDataChanged(({ key, value, origin }) => {
  console.log(`Data for ${key} changed in tab ${origin}`);
  // Update UI or app state accordingly
});
          
        

5. Advanced Usage

Offline-First Application Data

The adapter is ideal for managing offline-first application data:

          
// Store application data for offline use
async function cacheApplicationData(data) {
  await adapter.set("app.cache", data);
  await adapter.set("app.lastSyncTime", Date.now());
}

// Retrieve cached data when offline
async function getOfflineData() {
  return await adapter.get("app.cache");
}

// Check if cache needs refreshing (older than 1 day)
async function shouldRefreshCache() {
  const lastSync = await adapter.get("app.lastSyncTime");
  if (!lastSync) return true;
  const oneDay = 24 * 60 * 60 * 1000;
  return (Date.now() - lastSync) > oneDay;
}
          
        

User Preferences Management

Managing persistent user preferences:

          
// Store user preferences
async function saveUserPreferences(preferences) {
  await adapter.set("preferences", preferences);
}

// Load user preferences
async function loadUserPreferences() {
  return await adapter.get("preferences") || getDefaultPreferences();
}

// Update a single preference
async function updatePreference(key, value) {
  const prefs = await loadUserPreferences();
  prefs[key] = value;
  await saveUserPreferences(prefs);
}
          
        

Complex Data Structures with Prefixes

Managing related data using key prefixes:

          
// Store product data
async function saveProduct(product) {
  await adapter.set(`product.${product.id}`, product);
}

// Add product to favorites
async function addToFavorites(productId) {
  await adapter.set(`favorite.${productId}`, true);
}

// Get all favorite products
async function getFavoriteProducts() {
  const favorites = await adapter.list("favorite.");
  const productIds = favorites.map(f => f.key.replace("favorite.", ""));
  
  const products = [];
  for (const id of productIds) {
    const product = await adapter.get(`product.${id}`);
    if (product) products.push(product);
  }
  
  return products;
}
          
        

IndexedDB with Encryption for Sensitive Data

Secure storage of sensitive user information:

          
// Create a separate adapter instance for sensitive data
const secureAdapter = new IndexedDBAdapter({
  dbName: "secure_storage",
  encrypt: true
});

// Store encrypted credentials
async function storeCredentials(username, accessToken, refreshToken) {
  await secureAdapter.set("auth.username", username);
  await secureAdapter.set("auth.accessToken", accessToken);
  await secureAdapter.set("auth.refreshToken", refreshToken);
  await secureAdapter.set("auth.timestamp", Date.now());
}

// Get authentication data
async function getAuthData() {
  const authData = await secureAdapter.list("auth.");
  return authData.reduce((obj, item) => {
    obj[item.key.replace("auth.", "")] = item.value;
    return obj;
  }, {});
}

// Clear authentication on logout
async function logout() {
  const authKeys = await secureAdapter.list("auth.");
  for (const item of authKeys) {
    await secureAdapter.delete(item.key);
  }
}
          
        

5. Comparison with Other Storage Adapters

Feature IndexedDBAdapter LocalStorageAdapter SessionStorageAdapter
Persistence Persistent until explicitly cleared Persistent until explicitly cleared Until tab/browser closes
Storage Capacity Large (typically 50MB-unlimited) ~5MB per origin ~5MB per origin
Performance Optimized for large datasets Fast for small data Fast for small data
Complexity More complex, async API Simple, synchronous underlying API Simple, synchronous underlying API
Best Use Cases Large datasets, offline apps, complex data structures User preferences, app settings Form state, wizards, temporary authentication
Browser Support All modern browsers All browsers All browsers
Cross-Tab Sync Requires explicit channelName Requires explicit channelName Requires explicit channelName

6. Notes

  • IndexedDB operations are asynchronous - always use await when calling adapter methods.
  • Consider calling waitReadiness() after initialization to ensure the database is open before performing operations.
  • Use encrypt: true for sensitive data that should be protected even in the client-side storage.
  • Keys are namespaced under {dbName}__ to avoid conflicts.
  • A beforeunload event listener is registered to properly clean up resources.
  • Expirations are automatically checked and cleared at the specified interval (default 1s).
  • For very large datasets, consider implementing pagination or lazy loading patterns.
  • IndexedDB has excellent support across modern browsers but may require polyfills for older browsers.
  • For complex data models, consider using multiple adapters with different store names within the same database.