Skip to main content
โšก Calmops

Complete Guide to Browser Storage Mechanisms: Making the Right Choice

Table of Contents

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

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

  1. Check Storage Availability: Always detect API support before using storage.
  2. Handle Quota Exceeded: Implement error handling for storage quota scenarios.
  3. Data Validation: Validate and sanitize data before storing or retrieving.
  4. Clear Sensitive Data: Remove sensitive information when appropriate.
  5. 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:

  1. Simple Use Cases: Local Storage remains the go-to for basic key-value persistence.

  2. Large Datasets: IndexedDB is essential for applications requiring substantial data storage and complex queries.

  3. Authentication: Cookies are the standard for authentication tokens, especially with HttpOnly flags.

  4. Privacy-First: Leverage Privacy Sandbox APIs (Interest Groups, Shared Storage) when targeting or analytics are necessary.

  5. Extensions: Extension Storage APIs provide tailored storage solutions for browser extensions.

  6. 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
  7. 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

Comments