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
wait the animation to finish before go to another tab!
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.