Introduction: Why Browser Storage Matters
In modern web applications, client-side storage is fundamental to creating responsive, offline-capable, and user-friendly experiences. Whether you need to persist user preferences, cache API responses, maintain session data, or handle complex application state, choosing the right browser storage mechanism can significantly impact your application’s performance, privacy, and user experience.
The web platform offers diverse storage solutions, each optimized for different use cases. Understanding these optionsโfrom simple key-value stores to sophisticated structured databasesโis essential for making informed architectural decisions.
This guide explores nine major browser storage mechanisms, their capabilities, limitations, and practical applications. By the end, you’ll confidently select the appropriate storage solution for your specific requirements.
1. Local Storage
Definition and Purpose
Local Storage is a synchronous, key-value storage API that persists data indefinitely until explicitly cleared. Data stored in Local Storage remains available across browser sessions and tab closures, making it ideal for long-term client-side persistence.
Key Characteristics:
- Capacity: Typically 5-10 MB per origin
- Persistence: Until manually cleared or browser data is wiped
- Scope: Per origin (protocol + domain + port)
- Access: Synchronous JavaScript API
- Expiration: None (permanent until cleared)
Storage Capacity and Limits
Most modern browsers allocate 5-10 MB per origin for Local Storage. However, capacity varies:
- Chrome, Edge, Firefox: ~10 MB
- Safari: ~5 MB
- Internet Explorer: ~10 MB
Exceeding the quota throws a QuotaExceededError.
When to Use Local Storage
- User preferences and settings
- UI state (collapsed/expanded sections, theme preferences)
- Non-sensitive cached data
- Recently viewed items
- Draft content (before server submission)
Code Examples
Basic CRUD Operations
// CREATE/UPDATE
function saveUserPreferences(preferences) {
try {
const data = JSON.stringify(preferences);
localStorage.setItem('userPreferences', data);
console.log('Preferences saved successfully');
} catch (error) {
if (error.name === 'QuotaExceededError') {
console.error('Storage quota exceeded');
} else {
console.error('Error saving preferences:', error);
}
}
}
// READ
function getUserPreferences() {
try {
const stored = localStorage.getItem('userPreferences');
return stored ? JSON.parse(stored) : null;
} catch (error) {
console.error('Error reading preferences:', error);
return null;
}
}
// UPDATE
function updateTheme(theme) {
const preferences = getUserPreferences() || {};
preferences.theme = theme;
saveUserPreferences(preferences);
}
// DELETE
function clearPreferences() {
localStorage.removeItem('userPreferences');
}
// DELETE ALL
function clearAllStorage() {
localStorage.clear();
}
Detecting Storage Availability
function isLocalStorageAvailable() {
try {
const test = '__localStorage_test__';
localStorage.setItem(test, test);
localStorage.removeItem(test);
return true;
} catch (error) {
return false;
}
}
// Usage
if (isLocalStorageAvailable()) {
localStorage.setItem('userTheme', 'dark');
} else {
console.warn('Local Storage not available, using in-memory storage');
}
Cache Management with Expiration
function setWithExpiration(key, value, expirationMinutes) {
const item = {
value: value,
expiration: Date.now() + (expirationMinutes * 60 * 1000)
};
localStorage.setItem(key, JSON.stringify(item));
}
function getWithExpiration(key) {
const stored = localStorage.getItem(key);
if (!stored) return null;
try {
const item = JSON.parse(stored);
if (Date.now() > item.expiration) {
localStorage.removeItem(key);
return null;
}
return item.value;
} catch (error) {
console.error('Error retrieving item:', error);
return null;
}
}
// Usage
setWithExpiration('apiCache', { data: 'response' }, 30); // 30 minutes
const cachedData = getWithExpiration('apiCache');
2. Session Storage
Definition and Purpose
Session Storage functions identically to Local Storage but with a critical difference: data is cleared when the tab or window closes. It’s designed for temporary data that should not persist beyond the current session.
Key Characteristics:
- Capacity: Typically 5-10 MB per origin
- Persistence: Cleared when tab/window closes
- Scope: Per tab/window and origin
- Access: Synchronous JavaScript API
- Isolation: Data isolated between tabs
Storage Capacity and Limits
Session Storage has the same size limits as Local Storage:
- Most browsers: 5-10 MB per origin per tab
When to Use Session Storage
- Temporary form data during multi-step workflows
- Session-specific tokens (not sensitive auth tokens)
- Transient UI state
- Data that shouldn’t persist across page reloads
- Tab-specific application state
Code Examples
Multi-Step Form Workflow
const FORM_STORAGE_KEY = 'multiStepForm';
function saveFormStep(stepNumber, formData) {
try {
const formState = JSON.parse(sessionStorage.getItem(FORM_STORAGE_KEY)) || {};
formState[`step${stepNumber}`] = formData;
sessionStorage.setItem(FORM_STORAGE_KEY, JSON.stringify(formState));
} catch (error) {
console.error('Error saving form step:', error);
}
}
function getFormStep(stepNumber) {
try {
const formState = JSON.parse(sessionStorage.getItem(FORM_STORAGE_KEY)) || {};
return formState[`step${stepNumber}`] || null;
} catch (error) {
console.error('Error retrieving form step:', error);
return null;
}
}
function completeForm() {
const formState = JSON.parse(sessionStorage.getItem(FORM_STORAGE_KEY)) || {};
sessionStorage.removeItem(FORM_STORAGE_KEY);
return formState;
}
// Usage
saveFormStep(1, { name: 'John', email: '[email protected]' });
saveFormStep(2, { address: '123 Main St', city: 'New York' });
const completeFormData = completeForm();
Session Activity Tracking
function trackSessionActivity() {
const activity = {
startTime: Date.now(),
lastActiveTime: Date.now(),
pageViews: 0,
interactions: []
};
sessionStorage.setItem('sessionActivity', JSON.stringify(activity));
}
function recordInteraction(type, details) {
try {
const activity = JSON.parse(sessionStorage.getItem('sessionActivity')) || {};
activity.lastActiveTime = Date.now();
activity.interactions.push({
type: type,
details: details,
timestamp: Date.now()
});
sessionStorage.setItem('sessionActivity', JSON.stringify(activity));
} catch (error) {
console.error('Error recording interaction:', error);
}
}
function getSessionDuration() {
try {
const activity = JSON.parse(sessionStorage.getItem('sessionActivity')) || {};
return activity.lastActiveTime - activity.startTime;
} catch (error) {
return 0;
}
}
// Usage
trackSessionActivity();
document.addEventListener('click', () => {
recordInteraction('click', { target: event.target.className });
});
3. IndexedDB
Definition and Purpose
IndexedDB is a low-level API for storing large amounts of structured data on the client side. Unlike Local Storage’s simple key-value model, IndexedDB supports complex queries, indexing, and transactionsโmaking it suitable for offline applications and complex data management.
Key Characteristics:
- Capacity: Typically 50+ MB, often with user permission for unlimited storage
- Persistence: Until explicitly cleared
- Scope: Per origin
- Access: Asynchronous API (Promise-based)
- Data Structure: Object stores with indexes and transactions
- Features: Transactions, indexes, complex queries, full-text search support
Storage Capacity and Limits
- Chrome, Edge, Firefox: Typically 50% of available disk space
- Safari: ~50 MB without user permission, unlimited with permission
- Dynamic quota: Browsers may adjust based on available storage
When to Use IndexedDB
- Large datasets that require querying
- Offline-first applications
- Complex application state
- Caching large API responses
- Real-time collaborative applications
- Financial/analytics applications with significant data
Code Examples
Database Setup and CRUD Operations
class DatabaseManager {
constructor(dbName = 'myAppDB', version = 1) {
this.dbName = dbName;
this.version = version;
this.db = null;
}
// Initialize database with stores
async init() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve(this.db);
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
// Create object store for users
if (!db.objectStoreNames.contains('users')) {
const userStore = db.createObjectStore('users', { keyPath: 'id' });
userStore.createIndex('email', 'email', { unique: true });
userStore.createIndex('name', 'name', { unique: false });
}
// Create object store for posts
if (!db.objectStoreNames.contains('posts')) {
const postStore = db.createObjectStore('posts', { keyPath: 'id', autoIncrement: true });
postStore.createIndex('userId', 'userId', { unique: false });
postStore.createIndex('createdAt', 'createdAt', { unique: false });
}
};
});
}
// CREATE
async addUser(user) {
const transaction = this.db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
return new Promise((resolve, reject) => {
const request = store.add(user);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// READ - Get by primary key
async getUser(id) {
const transaction = this.db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
return new Promise((resolve, reject) => {
const request = store.get(id);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// READ - Query by index
async getUserByEmail(email) {
const transaction = this.db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const index = store.index('email');
return new Promise((resolve, reject) => {
const request = index.get(email);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// READ - Get all
async getAllUsers() {
const transaction = this.db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
return new Promise((resolve, reject) => {
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// UPDATE
async updateUser(user) {
const transaction = this.db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
return new Promise((resolve, reject) => {
const request = store.put(user);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// DELETE
async deleteUser(id) {
const transaction = this.db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
return new Promise((resolve, reject) => {
const request = store.delete(id);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
// TRANSACTION - Multiple operations
async createUserWithPosts(user, posts) {
const transaction = this.db.transaction(['users', 'posts'], 'readwrite');
return new Promise((resolve, reject) => {
const userStore = transaction.objectStore('users');
const postStore = transaction.objectStore('posts');
const userRequest = userStore.add(user);
userRequest.onsuccess = () => {
const userId = userRequest.result;
const postsToAdd = posts.map(post => ({ ...post, userId }));
postsToAdd.forEach(post => {
postStore.add(post);
});
};
transaction.oncomplete = () => resolve(userRequest.result);
transaction.onerror = () => reject(transaction.error);
});
}
}
// Usage
const db = new DatabaseManager('contentDB', 1);
(async () => {
await db.init();
// Add user
await db.addUser({ id: 1, name: 'Alice', email: '[email protected]' });
// Get user
const user = await db.getUser(1);
console.log('User:', user);
// Update user
user.name = 'Alice Updated';
await db.updateUser(user);
// Get all users
const allUsers = await db.getAllUsers();
console.log('All users:', allUsers);
// Delete user
await db.deleteUser(1);
})();
Complex Queries with Range and Filtering
// Query posts within date range
async function getPostsInDateRange(startDate, endDate) {
const transaction = this.db.transaction(['posts'], 'readonly');
const store = transaction.objectStore('posts');
const index = store.index('createdAt');
const range = IDBKeyRange.bound(startDate, endDate);
return new Promise((resolve, reject) => {
const request = index.getAll(range);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// Cursor-based iteration with filtering
async function getPostsByUserWithFilter(userId, filter) {
const transaction = this.db.transaction(['posts'], 'readonly');
const store = transaction.objectStore('posts');
const index = store.index('userId');
return new Promise((resolve, reject) => {
const results = [];
const request = index.openCursor(IDBKeyRange.only(userId));
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
const post = cursor.value;
if (filter(post)) {
results.push(post);
}
cursor.continue();
} else {
resolve(results);
}
};
request.onerror = () => reject(request.error);
});
}
4. Cookies
Definition and Purpose
Cookies are small text files stored on the client side and automatically sent with every HTTP request to the server. They’re versatile for authentication, tracking, and storing user preferences, though they have size limitations and security considerations.
Key Characteristics:
- Capacity: Typically 4 KB per cookie, ~180 cookies per domain
- Persistence: Configurable (session-based or with expiration date)
- Scope: Per domain (can specify path and subdomains)
- Transmission: Automatically sent with HTTP requests
- Access: Both JavaScript (with restrictions) and server-side
- Security: Can be flagged as HttpOnly, Secure, SameSite
Storage Capacity and Limits
- Per-cookie limit: 4 KB
- Per-domain limit: ~180 cookies
- Total per-domain limit: ~4 KB total (varies by browser)
When to Use Cookies
- Authentication tokens
- Session management
- User tracking and analytics
- Remembering user choices
- Cross-domain data sharing (limited by SameSite)
- Language/locale preferences
Code Examples
Basic Cookie Operations
class CookieManager {
// SET - Create or update cookie
static set(name, value, options = {}) {
let cookieString = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
if (options.expires) {
cookieString += `; expires=${options.expires.toUTCString()}`;
}
if (options.maxAge) {
cookieString += `; max-age=${options.maxAge}`;
}
if (options.path) {
cookieString += `; path=${options.path}`;
}
if (options.domain) {
cookieString += `; domain=${options.domain}`;
}
if (options.secure) {
cookieString += '; secure';
}
if (options.sameSite) {
cookieString += `; samesite=${options.sameSite}`;
}
document.cookie = cookieString;
}
// GET - Retrieve cookie value
static get(name) {
const nameEQ = `${encodeURIComponent(name)}=`;
const cookies = document.cookie.split(';');
for (let cookie of cookies) {
cookie = cookie.trim();
if (cookie.startsWith(nameEQ)) {
return decodeURIComponent(cookie.substring(nameEQ.length));
}
}
return null;
}
// GET ALL - Retrieve all cookies
static getAll() {
const cookieObj = {};
const cookies = document.cookie.split(';');
for (let cookie of cookies) {
cookie = cookie.trim();
const [name, value] = cookie.split('=');
if (name && value) {
cookieObj[decodeURIComponent(name)] = decodeURIComponent(value);
}
}
return cookieObj;
}
// DELETE - Remove cookie
static delete(name, options = {}) {
this.set(name, '', {
...options,
maxAge: -1
});
}
// DELETE ALL
static deleteAll() {
const cookies = document.cookie.split(';');
for (let cookie of cookies) {
const name = cookie.split('=')[0].trim();
this.delete(name);
}
}
}
// Usage
// Set authentication cookie (expires in 7 days)
CookieManager.set('authToken', 'abc123xyz', {
maxAge: 7 * 24 * 60 * 60,
path: '/',
sameSite: 'Strict',
secure: true
});
// Get cookie
const token = CookieManager.get('authToken');
// Set persistent user preference
CookieManager.set('userTheme', 'dark', {
expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days
path: '/'
});
// Delete cookie
CookieManager.delete('authToken');
Secure Authentication Cookies
// Server-side set (example with Node.js/Express)
// response.cookie('sessionId', sessionId, {
// httpOnly: true, // Not accessible via JavaScript
// secure: true, // HTTPS only
// sameSite: 'strict', // CSRF protection
// maxAge: 3600000 // 1 hour
// });
// Client-side verification (httpOnly cookies not visible)
class AuthManager {
static async login(credentials) {
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include', // Send cookies
body: JSON.stringify(credentials)
});
if (response.ok) {
return await response.json();
} else {
throw new Error('Login failed');
}
} catch (error) {
console.error('Authentication error:', error);
return null;
}
}
static async logout() {
try {
await fetch('/api/logout', {
method: 'POST',
credentials: 'include'
});
} catch (error) {
console.error('Logout error:', error);
}
}
}
// Usage
await AuthManager.login({ email: '[email protected]', password: 'password' });
5. Extension Storage API
Definition and Purpose
The Extension Storage API is a specialized storage mechanism for browser extensions, offering more capacity than standard localStorage with built-in sync capabilities. It’s designed specifically for extensions to store configuration data, user preferences, and extension-specific information.
Key Characteristics:
- Capacity: 10 MB (unlocked extensions), up to 100 MB with storage permission
- Persistence: Until cleared by user or extension
- Scope: Per extension
- Access: Asynchronous Promise-based API
- Features: Built-in sync support (Chrome/Edge), managed vs. local storage areas
- Privacy: Isolated from web content storage
Storage Capacity and Limits
- Local area: 10 MB
- Sync area: 100 KB (synced across devices)
- Managed area: 5 MB (set by enterprise policies)
When to Use Extension Storage
- Browser extension configuration
- Extension-specific preferences
- Cross-device settings (with sync)
- Extension user data
- Extension state and caches
Code Examples
Extension Storage Implementation
// manifest.json
{
"manifest_version": 3,
"permissions": ["storage"],
"name": "My Extension",
"version": "1.0"
}
// Background/Service Worker Script
class ExtensionStorageManager {
constructor() {
this.storage = chrome.storage.local; // or chrome.storage.sync
}
// SET - Store data
async set(key, value) {
return new Promise((resolve, reject) => {
chrome.storage.local.set({ [key]: value }, () => {
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError);
} else {
resolve();
}
});
});
}
// GET - Retrieve data
async get(key) {
return new Promise((resolve, reject) => {
chrome.storage.local.get(key, (result) => {
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError);
} else {
resolve(result[key]);
}
});
});
}
// GET MULTIPLE
async getMultiple(keys) {
return new Promise((resolve, reject) => {
chrome.storage.local.get(keys, (result) => {
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError);
} else {
resolve(result);
}
});
});
}
// UPDATE - Partial update
async update(key, updates) {
const current = await this.get(key) || {};
const merged = { ...current, ...updates };
return this.set(key, merged);
}
// DELETE
async delete(key) {
return new Promise((resolve, reject) => {
chrome.storage.local.remove(key, () => {
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError);
} else {
resolve();
}
});
});
}
// CLEAR ALL
async clear() {
return new Promise((resolve, reject) => {
chrome.storage.local.clear(() => {
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError);
} else {
resolve();
}
});
});
}
// LISTEN for changes
onChanged(callback) {
chrome.storage.onChanged.addListener((changes, areaName) => {
callback(changes, areaName);
});
}
}
// Usage
const extStorage = new ExtensionStorageManager();
(async () => {
// Store extension settings
await extStorage.set('settings', {
theme: 'dark',
notifications: true,
apiKey: 'user-api-key'
});
// Retrieve settings
const settings = await extStorage.get('settings');
console.log('Settings:', settings);
// Update specific setting
await extStorage.update('settings', { theme: 'light' });
// Listen for changes
extStorage.onChanged((changes, areaName) => {
for (const [key, { oldValue, newValue }] of Object.entries(changes)) {
console.log(`${key} changed from ${oldValue} to ${newValue}`);
}
});
})();
6. Private State Tokens
Definition and Purpose
Private State Tokens (formerly “Trust Tokens”) is a privacy-preserving mechanism that allows websites to acquire non-identifying trust signals about users. Unlike cookies or identifiers, tokens don’t allow tracking; they merely indicate that a user is trustworthy (to prevent fraud or spam).
Key Characteristics:
- Capacity: Limited number of tokens per site
- Persistence: Duration defined by issuer
- Scope: One-time use per token
- Privacy: No user identifier information
- Use: Anti-fraud, bot detection, bypassing CAPTCHA
- Redemption: Server-side verification required
- Browser Support: Limited (Chrome, Edge with flag enabled)
Storage Capacity and Limits
- Token limit: Typically 500 issuers, max 10 tokens per issuer
- Validity period: Set by issuer
When to Use Private State Tokens
- Bot and fraud detection
- Building trust indicators
- Conditional CAPTCHA bypass
- Privacy-preserving reputation systems
Code Examples
Private State Tokens Implementation
// Client-side Token Issuance
class PrivateStateTokenClient {
// Request tokens from issuer
static async requestTokens(issuerOrigin, count = 1) {
try {
const response = await fetch(`${issuerOrigin}/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include', // Include cookies for cross-origin
body: JSON.stringify({ count: count })
});
if (!response.ok) {
throw new Error('Failed to request tokens');
}
return await response.json();
} catch (error) {
console.error('Token request error:', error);
return null;
}
}
// Redeem tokens
static async redeemTokens(redeemingOrigin, tokens) {
try {
const response = await fetch(`${redeemingOrigin}/redeem`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({
tokens: tokens,
redemptionRecord: 'record-data'
})
});
if (!response.ok) {
throw new Error('Failed to redeem tokens');
}
return await response.json();
} catch (error) {
console.error('Token redemption error:', error);
return null;
}
}
// Check token availability
static async hasTokens(issuerOrigin) {
try {
const response = await fetch(
`${issuerOrigin}/.well-known/private-state-token`,
{ method: 'HEAD' }
);
return response.ok;
} catch (error) {
console.error('Token check error:', error);
return false;
}
}
}
// Usage
(async () => {
// Request tokens from issuer
const issuerOrigin = 'https://issuer.example.com';
const tokenData = await PrivateStateTokenClient.requestTokens(issuerOrigin, 10);
console.log('Tokens issued:', tokenData);
// Later, redeem tokens when accessing resource
const redeemingOrigin = 'https://resource-provider.example.com';
const result = await PrivateStateTokenClient.redeemTokens(redeemingOrigin, tokenData.tokens);
console.log('Redemption result:', result);
})();
// Server-side validation (Node.js/Express example)
app.post('/verify-token', (req, res) => {
const { tokenRedemptionRecord } = req.body;
// Verify token signature and issuer
// Check redemption record
// Verify token hasn't been reused
if (isValidToken(tokenRedemptionRecord)) {
res.json({ trusted: true, message: 'User verified as trustworthy' });
} else {
res.status(401).json({ trusted: false });
}
});
7. Interest Groups (Federated Learning of Cohorts - FLoC Alternative)
Definition and Purpose
Interest Groups API (replacement for FLoC) allows advertisers to create groups of users interested in topics for targeting ads without identifying individuals. Data is stored locally on the user’s browser and used for serving contextual ads.
Key Characteristics:
- Capacity: Multiple interest groups per user
- Persistence: User-defined duration
- Scope: Per-browser, not per-site
- Privacy: No personal identification
- Use: Ad targeting, cohort-based advertising
- Access: JavaScript API for joining groups
- Browser Support: Chrome/Edge with Privacy Sandbox enabled
Storage Capacity and Limits
- Interest group limit: Up to 1,000 groups
- Group size: Limited data per group
- Duration: User-configurable, typically 30 days
When to Use Interest Groups
- Privacy-preserving ad targeting
- Interest-based audience segmentation
- Remarketing without third-party cookies
- Cohort advertising
Code Examples
Interest Groups Implementation
// Join an interest group
async function joinInterestGroup() {
// Check API availability
if (navigator.joinAdInterestGroup) {
try {
await navigator.joinAdInterestGroup({
owner: 'https://advertiser.example.com',
name: 'tech-enthusiasts',
// User signals/data
userBiddingSignals: {
interests: ['technology', 'gadgets', 'ai'],
purchaseHistory: ['laptop', 'phone'],
engagementLevel: 'high'
},
// Duration in milliseconds
lifetimeMs: 30 * 24 * 60 * 60 * 1000, // 30 days
// Bidding and reporting URLs
biddingLogicUrl: 'https://advertiser.example.com/bidding-logic.js',
biddingWasmHelperUrl: 'https://advertiser.example.com/bidding-logic.wasm',
// Daily update URL
dailyUpdateUrl: 'https://advertiser.example.com/update',
// Trusted bidding signals
trustedBiddingSignalsUrl: 'https://advertiser.example.com/bidding-signals',
trustedBiddingSignalsKeys: ['key1', 'key2'],
// Ads for display
ads: [
{
renderUrl: 'https://advertiser.example.com/ad1.html',
metadata: JSON.stringify({ title: 'Amazing Laptop' })
}
]
}, 30 * 24 * 60 * 60); // 30 days
console.log('Successfully joined interest group');
} catch (error) {
console.error('Error joining interest group:', error);
}
} else {
console.warn('Interest Groups API not supported');
}
}
// Leave an interest group
async function leaveInterestGroup() {
if (navigator.leaveAdInterestGroup) {
try {
await navigator.leaveAdInterestGroup({
owner: 'https://advertiser.example.com',
name: 'tech-enthusiasts'
});
console.log('Successfully left interest group');
} catch (error) {
console.error('Error leaving interest group:', error);
}
}
}
// Update interest group membership
async function updateInterestGroupMembership(interestCategory, shouldJoin) {
const groupName = `category-${interestCategory}`;
if (shouldJoin) {
await joinInterestGroup(groupName);
} else {
await leaveInterestGroup(groupName);
}
}
// Usage
joinInterestGroup();
setTimeout(() => {
leaveInterestGroup();
}, 5 * 60 * 1000); // Leave after 5 minutes
8. Shared Storage
Definition and Purpose
Shared Storage is a cross-site storage mechanism with privacy protections. It allows limited, privacy-preserving access to shared data across sites without identifying users. Data operations are restricted to prevent tracking.
Key Characteristics:
- Capacity: 4 KB per origin, 100 MB total
- Persistence: User-configurable
- Scope: Cross-site access with privacy protections
- Access: Limited to write and read operations, no direct enumeration
- Privacy: Restricts data leakage through output encoding
- Use Cases: A/B testing, reach reporting, frequency capping
- Browser Support: Chrome/Edge with Privacy Sandbox enabled
Storage Capacity and Limits
- Per-origin quota: 4 KB
- Total quota: 100 MB
- Operation rate limits: Throttled to prevent timing attacks
When to Use Shared Storage
- Cross-site A/B testing
- Ad frequency capping
- Analytics and reach measurement
- URL redirection for tracking
- Cross-site audience analysis
Code Examples
Shared Storage Implementation
// Write to Shared Storage
async function writeSharedStorageData() {
if (!window.sharedStorage) {
console.warn('Shared Storage not available');
return;
}
try {
// Write test data
await window.sharedStorage.set('ab-test-group', 'control');
console.log('Data written to Shared Storage');
// Append to list
await window.sharedStorage.append('visited-domains', 'example.com');
await window.sharedStorage.append('visited-domains', 'test.com');
// Increment counter
await window.sharedStorage.set('ad-frequency', '0');
await window.sharedStorage.increment('ad-frequency');
await window.sharedStorage.increment('ad-frequency');
} catch (error) {
console.error('Error writing to Shared Storage:', error);
}
}
// Read from Shared Storage (via operation)
async function selectFromSharedStorage() {
if (!window.sharedStorage) {
console.warn('Shared Storage not available');
return;
}
try {
// Load script that performs the read
await window.sharedStorage.selectURL(
'https://advertiser.example.com/select-operation.js',
[
{ url: 'https://advertiser.example.com/ad-slot1.html' },
{ url: 'https://advertiser.example.com/ad-slot2.html' }
],
{
resolveToConfig: true,
data: { experiment: 'ab-test' }
}
);
} catch (error) {
console.error('Error selecting from Shared Storage:', error);
}
}
// Operation script for Shared Storage read
class SelectOperation {
async run(data) {
try {
// Read value from Shared Storage
const testGroup = await this.sharedStorage.get('ab-test-group');
console.log('Test group:', testGroup);
// Select URL based on stored data
if (testGroup === 'control') {
return 0; // Return first URL
} else {
return 1; // Return second URL
}
} catch (error) {
console.error('Operation error:', error);
return 0;
}
}
}
// Register operation in select-operation.js
register('SelectOperation', SelectOperation);
Shared Storage for Frequency Capping
// Shared Storage script for frequency capping
class FrequencyCapOperation {
async run(data) {
const { maxFrequency } = data;
try {
// Get current frequency
const currentFrequency = await this.sharedStorage.get('ad-impressions');
const count = parseInt(currentFrequency || '0', 10);
console.log(`Ad impressions: ${count}`);
// Check if frequency limit exceeded
if (count >= maxFrequency) {
return 1; // Return index for 'do not show' URL
}
// Increment counter
await this.sharedStorage.increment('ad-impressions');
return 0; // Return index for 'show ad' URL
} catch (error) {
console.error('Frequency cap error:', error);
return 0;
}
}
}
register('FrequencyCapOperation', FrequencyCapOperation);
// Main page code
async function applyFrequencyCap(maxAdsPerDay = 3) {
if (!window.sharedStorage) return;
const result = await window.sharedStorage.selectURL(
'https://advertiser.example.com/frequency-cap-operation.js',
[
{ url: 'https://advertiser.example.com/show-ad.html' },
{ url: 'https://advertiser.example.com/no-ad.html' }
],
{ data: { maxFrequency: maxAdsPerDay } }
);
return result;
}
9. Storage Buckets
Definition and Purpose
Storage Buckets is an API that allows developers to create and manage multiple independent storage areas within a single origin. Each bucket can have its own persistence mode and durability guarantees, providing fine-grained control over storage lifecycle.
Key Characteristics:
- Capacity: Multiple buckets per origin, each with dedicated quota
- Persistence: Configurable per bucket (persistent, best-effort, temporary)
- Scope: Per origin
- Features: Bucket-specific quota management, independent expiration
- Use Cases: Separating user data from cache, granular storage control
- Browser Support: Chrome/Edge (experimental)
Storage Capacity and Limits
- Default bucket: Shared across all storage APIs
- Custom buckets: Separate quotas manageable independently
- Duration modes: Persistent (user permission required), temporary (auto-cleanup)
When to Use Storage Buckets
- Separating permanent user data from temporary cache
- Granular quota management
- Automatic cleanup of temporary storage
- Multiple storage policies in one application
- Cache management with different durability levels
Code Examples
Storage Buckets Implementation
class StorageBucketManager {
// Create a new bucket
async createBucket(name, options = {}) {
try {
const bucket = await navigator.storage.bucket(name);
// Configure bucket
if (bucket) {
console.log(`Bucket "${name}" created/opened`);
return bucket;
}
} catch (error) {
console.error('Error creating bucket:', error);
return null;
}
}
// Get bucket
async getBucket(name) {
try {
const bucket = await navigator.storage.bucket(name);
return bucket;
} catch (error) {
console.error('Error getting bucket:', error);
return null;
}
}
// Set data in bucket
async setInBucket(bucketName, storageType, key, value) {
try {
const bucket = await this.getBucket(bucketName);
if (!bucket) throw new Error('Bucket not found');
if (storageType === 'localStorage') {
const storage = await bucket.getDirectory();
// Use IndexedDB within bucket scope
const db = await this.initBucketDB(bucket);
return this.setInDB(db, key, value);
}
} catch (error) {
console.error('Error setting bucket data:', error);
}
}
// Get bucket quota
async getBucketQuota(bucketName) {
try {
const bucket = await this.getBucket(bucketName);
if (!bucket) throw new Error('Bucket not found');
const estimate = await bucket.estimate();
return {
usage: estimate.usage,
quota: estimate.quota,
percentage: (estimate.usage / estimate.quota) * 100
};
} catch (error) {
console.error('Error getting bucket quota:', error);
return null;
}
}
// Delete bucket
async deleteBucket(bucketName) {
try {
await navigator.storage.delete(bucketName);
console.log(`Bucket "${bucketName}" deleted`);
} catch (error) {
console.error('Error deleting bucket:', error);
}
}
// List all buckets
async listBuckets() {
try {
const buckets = await navigator.storage.buckets();
return buckets || [];
} catch (error) {
console.error('Error listing buckets:', error);
return [];
}
}
// Initialize IndexedDB within bucket
async initBucketDB(bucket) {
return new Promise((resolve, reject) => {
const request = indexedDB.open('bucketDB', 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains('data')) {
db.createObjectStore('data');
}
};
});
}
async setInDB(db, key, value) {
const transaction = db.transaction(['data'], 'readwrite');
const store = transaction.objectStore('data');
return new Promise((resolve, reject) => {
const request = store.put(value, key);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
}
// Usage
(async () => {
const manager = new StorageBucketManager();
// Create buckets for different purposes
const userDataBucket = await manager.createBucket('user-data', {
durability: 'strict'
});
const cacheBucket = await manager.createBucket('cache', {
durability: 'relaxed'
});
// Check quota
const userDataQuota = await manager.getBucketQuota('user-data');
console.log('User data bucket:', userDataQuota);
// List all buckets
const allBuckets = await manager.listBuckets();
console.log('All buckets:', allBuckets);
// Delete bucket when done
// await manager.deleteBucket('cache');
})();
Comparison Table: Browser Storage Mechanisms
| Feature | Local Storage | Session Storage | IndexedDB | Cookies | Extension Storage | Private State Tokens | Interest Groups | Shared Storage | Storage Buckets |
|---|---|---|---|---|---|---|---|---|---|
| Capacity | 5-10 MB | 5-10 MB | 50+ MB | 4 KB per cookie | 10-100 MB | Limited | Multiple | 4 KB + 100 MB total | Configurable |
| Persistence | Indefinite | Until tab closes | Indefinite | Session/Expiration | Indefinite | Issuer-defined | 30 days | Configurable | Per-bucket |
| Scope | Per origin | Per tab/origin | Per origin | Domain/path-based | Per extension | One-time use | Per-browser | Cross-site limited | Per origin |
| API Type | Synchronous | Synchronous | Asynchronous | Synchronous | Asynchronous | Server-side | Client-side | Asynchronous | Asynchronous |
| Data Structure | Key-value (text) | Key-value (text) | Structured objects | Name-value pairs | Key-value (object) | Non-identifying tokens | Cohort assignments | Limited operations | Varied |
| Server Access | No | No | No | Yes | No | Yes (validation) | No | Limited | No |
| Use Cases | UI state, preferences | Form data, temp state | Complex data, offline | Auth, preferences | Extension config | Bot detection | Ad targeting | A/B testing, analytics | Granular storage control |
| Browser Support | All modern | All modern | All modern | All | Chrome/Edge | Chrome/Edge limited | Chrome/Edge limited | Chrome/Edge experimental | Chrome/Edge experimental |
| Privacy | Standard | Standard | Standard | Trackable | Extension-isolated | Privacy-preserving | Cohort-based | Privacy-preserving | Origin-isolated |
| Indexing | No | No | Yes (Indexes) | No | No | N/A | No | Limited | N/A |
| Transactions | No | No | Yes | No | No | N/A | No | No | No |
| Query Capability | No | No | Complex | Limited | No | N/A | No | Very limited | No |
When to Use Which Storage Mechanism: Decision Guide
Choose Local Storage if you need
- Simple key-value persistent storage
- Non-sensitive user preferences
- UI state across sessions
- Quick access to small amounts of data
- Maximum browser compatibility
- Example: User theme preference, sidebar state, recently viewed items
Choose Session Storage if you need
- Temporary data lasting only until tab closure
- Multi-step form workflows
- Session-specific state
- Tab-isolated data
- Example: Current form step, wizard progress, temporary session data
Choose IndexedDB if you need
- Storing large datasets (> 5 MB)
- Complex queries or filtering
- Offline-first application support
- Transaction support for data integrity
- Structured data with relationships
- Example: Offline email application, todo list with complex queries, media library
Choose Cookies if you need
- Automatic transmission with HTTP requests
- Server-side data access
- Cross-domain data sharing (with SameSite policies)
- Authentication tokens
- Example: Session tokens, language preferences, tracking IDs (with consent)
Choose Extension Storage if you need
- Browser extension-specific data
- Sync across user’s devices
- More than 5 MB storage for extensions
- Listener support for storage changes
- Example: Extension configuration, user settings, API keys
Choose Private State Tokens if you need
- Privacy-preserving trust signals
- Bot and fraud detection
- Avoiding CAPTCHA on trustworthy users
- No user identification
- Example: Trust scoring for access control, conditional CAPTCHAs
Choose Interest Groups if you need
- Privacy-preserving ad targeting
- Audience segmentation
- Replacement for third-party cookies (ads)
- Cohort-based advertising
- Example: Showing ads to interested users without tracking individuals
Choose Shared Storage if you need
- Cross-site analytics (with privacy)
- Ad frequency capping
- A/B testing across sites
- URL redirection with limited data
- Example: Measuring ad reach, capping ad frequency, cross-site experimentation
Choose Storage Buckets if you need
- Separating different storage types
- Fine-grained quota management
- Different persistence levels for different data
- Automatic cleanup policies
- Example: Keeping user data persistent while auto-cleaning cache
Best Practices and Security Considerations
General Guidelines
- Check Storage Availability: Always detect API support before using storage.
- Handle Quota Exceeded: Implement error handling for storage quota scenarios.
- Data Validation: Validate and sanitize data before storing or retrieving.
- Clear Sensitive Data: Remove sensitive information when appropriate.
- Respect User Privacy: Provide transparency about data collection and storage.
Security Best Practices
// Always check API availability
function isStorageAvailable(type) {
try {
const storage = window[type];
const test = '__storage_test__';
storage.setItem(test, test);
storage.removeItem(test);
return true;
} catch (error) {
return false;
}
}
// Encrypt sensitive data
async function encryptData(plaintext, password) {
const encoder = new TextEncoder();
const data = encoder.encode(plaintext);
// Use SubtleCrypto for encryption
const keyMaterial = await window.crypto.subtle.importKey(
'raw',
encoder.encode(password),
{ name: 'PBKDF2' },
false,
['deriveBits']
);
const salt = window.crypto.getRandomValues(new Uint8Array(16));
const key = await window.crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: salt,
iterations: 100000,
hash: 'SHA-256'
},
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt']
);
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const encrypted = await window.crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: iv },
key,
data
);
return {
salt: Array.from(salt),
iv: Array.from(iv),
data: Array.from(new Uint8Array(encrypted))
};
}
// Store encrypted data
async function storeSecureData(key, sensitiveData, password) {
try {
const encrypted = await encryptData(JSON.stringify(sensitiveData), password);
localStorage.setItem(key, JSON.stringify(encrypted));
} catch (error) {
console.error('Error storing secure data:', error);
}
}
// Implement storage cleanup
function setupStorageCleanup() {
const CLEANUP_INTERVAL = 7 * 24 * 60 * 60 * 1000; // 7 days
const lastCleanup = localStorage.getItem('lastStorageCleanup');
const now = Date.now();
if (!lastCleanup || (now - parseInt(lastCleanup)) > CLEANUP_INTERVAL) {
cleanupOldData();
localStorage.setItem('lastStorageCleanup', now.toString());
}
}
function cleanupOldData() {
const maxAge = 30 * 24 * 60 * 60 * 1000; // 30 days
const now = Date.now();
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
const item = localStorage.getItem(key);
try {
const data = JSON.parse(item);
if (data.timestamp && (now - data.timestamp) > maxAge) {
localStorage.removeItem(key);
}
} catch (error) {
// Skip non-JSON items
}
}
}
Conclusion: Making Informed Storage Decisions
Choosing the right browser storage mechanism is crucial for building performant, reliable, and privacy-respecting web applications. Here are the key takeaways:
-
Simple Use Cases: Local Storage remains the go-to for basic key-value persistence.
-
Large Datasets: IndexedDB is essential for applications requiring substantial data storage and complex queries.
-
Authentication: Cookies are the standard for authentication tokens, especially with HttpOnly flags.
-
Privacy-First: Leverage Privacy Sandbox APIs (Interest Groups, Shared Storage) when targeting or analytics are necessary.
-
Extensions: Extension Storage APIs provide tailored storage solutions for browser extensions.
-
Hybrid Approach: Modern applications often use multiple storage mechanisms:
- Cookies for authentication
- Local Storage for preferences
- IndexedDB for application data
- Private State Tokens for fraud detection
-
Always Consider:
- User privacy and consent
- Data security and encryption for sensitive information
- Storage quota and cleanup strategies
- Browser compatibility for your target audience
- Clear user communication about data collection
By understanding these storage mechanisms and their appropriate use cases, you can architect web applications that are efficient, user-friendly, and respectful of privacy concerns. Storage technology continues to evolve; staying informed about these mechanisms ensures your applications remain effective and compliant with emerging privacy standards.
Additional Resources
- MDN Web Docs - Web Storage API
- MDN - IndexedDB API
- Cookie Specification (RFC 6265)
- Chrome Privacy Sandbox
- W3C Storage Standard
Comments