Skip to main content
Technology & EngineeringBrowser Extension268 lines

Storage API

Extension storage APIs and state management patterns including sync, local, and session storage

Quick Summary20 lines
You are an expert in extension storage and state management for building browser extensions.

## Key Points

- Use `chrome.storage.sync` for user preferences that should follow the user across devices, and `chrome.storage.local` for larger data like caches or logs.
- Always provide default values when reading from storage to handle the case where keys have not been set yet.
- Batch related writes into a single `set()` call rather than making multiple calls, since each write has overhead and `sync` storage has rate limits.
- **Storing large binary data in `sync` storage** — the 100KB total and 8KB per-item limits are strict and silently enforced; large data belongs in `local` storage or IndexedDB.
- Exceeding `chrome.storage.sync` quotas (100 KB total, 8 KB per item) silently fails or throws — always keep synced data small and store large payloads in `local` instead.
- Treating `chrome.storage` like synchronous `localStorage` — all operations are asynchronous and must be awaited, or state may be stale or lost.

## Quick Example

```json
{
  "permissions": ["storage"]
}
```
skilldb get browser-extension-skills/Storage APIFull skill: 268 lines
Paste into your CLAUDE.md or agent config

Storage API — Browser Extension Development

You are an expert in extension storage and state management for building browser extensions.

Overview

Browser extensions have access to dedicated storage APIs that are separate from the web page's localStorage or sessionStorage. The chrome.storage API provides asynchronous, area-based storage that can sync across devices, persist locally, or live only for the browser session.

Core Concepts

Storage Areas

There are three storage areas, each with different scoping and limits:

AreaPersistenceSyncQuota
chrome.storage.localSurvives browser restartsNo10 MB (or unlimited with unlimitedStorage permission)
chrome.storage.syncSurvives restarts, syncs across devicesYes100 KB total, 8 KB per item, 1,800 writes/hour
chrome.storage.sessionCleared on browser restartNo10 MB
{
  "permissions": ["storage"]
}

Basic CRUD Operations

// SET — store one or more key-value pairs
await chrome.storage.local.set({
  userPrefs: { theme: 'dark', fontSize: 14 },
  lastVisit: Date.now()
});

// GET — retrieve by key(s), with optional defaults
const { userPrefs = {}, lastVisit } = await chrome.storage.local.get({
  userPrefs: {},
  lastVisit: null
});

// GET ALL
const allData = await chrome.storage.local.get(null);

// REMOVE — delete specific keys
await chrome.storage.local.remove(['lastVisit']);

// CLEAR — remove everything in the area
await chrome.storage.local.clear();

Listening for Changes

chrome.storage.onChanged.addListener((changes, areaName) => {
  for (const [key, { oldValue, newValue }] of Object.entries(changes)) {
    console.log(`[${areaName}] ${key}: ${JSON.stringify(oldValue)} -> ${JSON.stringify(newValue)}`);
  }
});

// Area-specific listener (Chrome 114+)
chrome.storage.local.onChanged.addListener((changes) => {
  if (changes.userPrefs) {
    applyTheme(changes.userPrefs.newValue.theme);
  }
});

Implementation Patterns

Settings Manager

A typed wrapper around storage for consistent access:

// settings.js
const DEFAULTS = {
  theme: 'system',
  notifications: true,
  blockedSites: [],
  syncInterval: 30
};

export const Settings = {
  async getAll() {
    const { settings = {} } = await chrome.storage.sync.get('settings');
    return { ...DEFAULTS, ...settings };
  },

  async get(key) {
    const all = await this.getAll();
    return all[key];
  },

  async set(updates) {
    const current = await this.getAll();
    const merged = { ...current, ...updates };
    await chrome.storage.sync.set({ settings: merged });
    return merged;
  },

  async reset() {
    await chrome.storage.sync.set({ settings: DEFAULTS });
    return DEFAULTS;
  },

  onChange(callback) {
    chrome.storage.sync.onChanged.addListener((changes) => {
      if (changes.settings) {
        callback(changes.settings.newValue, changes.settings.oldValue);
      }
    });
  }
};

Cache with Expiration

// cache.js
export const Cache = {
  async get(key) {
    const { [`cache_${key}`]: entry } = await chrome.storage.local.get(`cache_${key}`);
    if (!entry) return null;
    if (entry.expiry && Date.now() > entry.expiry) {
      await chrome.storage.local.remove(`cache_${key}`);
      return null;
    }
    return entry.value;
  },

  async set(key, value, ttlMinutes = 60) {
    const entry = {
      value,
      expiry: Date.now() + ttlMinutes * 60 * 1000,
      storedAt: Date.now()
    };
    await chrome.storage.local.set({ [`cache_${key}`]: entry });
  },

  async invalidate(key) {
    await chrome.storage.local.remove(`cache_${key}`);
  },

  async clearExpired() {
    const all = await chrome.storage.local.get(null);
    const expired = Object.keys(all).filter((k) =>
      k.startsWith('cache_') && all[k].expiry && Date.now() > all[k].expiry
    );
    if (expired.length > 0) {
      await chrome.storage.local.remove(expired);
    }
  }
};

Data Migration on Update

chrome.runtime.onInstalled.addListener(async (details) => {
  if (details.reason === 'update') {
    const { schemaVersion = 1 } = await chrome.storage.local.get('schemaVersion');

    if (schemaVersion < 2) {
      // Migrate from v1 to v2: rename 'prefs' to 'settings'
      const { prefs } = await chrome.storage.local.get('prefs');
      if (prefs) {
        await chrome.storage.local.set({ settings: prefs });
        await chrome.storage.local.remove('prefs');
      }
    }

    if (schemaVersion < 3) {
      // Migrate from v2 to v3: add new default field
      const { settings = {} } = await chrome.storage.local.get('settings');
      if (!settings.language) {
        settings.language = 'en';
        await chrome.storage.local.set({ settings });
      }
    }

    await chrome.storage.local.set({ schemaVersion: 3 });
  }
});

Reactive State with Storage Events

// Shared reactive state across popup, options, and content scripts
class StorageBackedState {
  constructor(area, key, defaults) {
    this.area = chrome.storage[area];
    this.key = key;
    this.defaults = defaults;
    this.listeners = new Set();

    this.area.onChanged.addListener((changes) => {
      if (changes[this.key]) {
        const newState = { ...this.defaults, ...changes[this.key].newValue };
        this.listeners.forEach((fn) => fn(newState));
      }
    });
  }

  async get() {
    const result = await this.area.get(this.key);
    return { ...this.defaults, ...(result[this.key] || {}) };
  }

  async update(partial) {
    const current = await this.get();
    const next = { ...current, ...partial };
    await this.area.set({ [this.key]: next });
    return next;
  }

  subscribe(fn) {
    this.listeners.add(fn);
    return () => this.listeners.delete(fn);
  }
}

// Usage
const appState = new StorageBackedState('local', 'appState', {
  isEnabled: false,
  count: 0,
  filter: 'all'
});

Best Practices

  • Use chrome.storage.sync for user preferences that should follow the user across devices, and chrome.storage.local for larger data like caches or logs.
  • Always provide default values when reading from storage to handle the case where keys have not been set yet.
  • Batch related writes into a single set() call rather than making multiple calls, since each write has overhead and sync storage has rate limits.

Core Philosophy

Extension storage is the backbone of state management in a world where every context — service workers, popups, content scripts, options pages — is ephemeral. Treat chrome.storage as your single source of truth, not as a secondary cache. Every piece of state that matters should live in storage, and every context should read from it on initialization rather than assuming it has been passed the latest value through messaging.

Choose the right storage area for each piece of data based on its lifecycle and audience. User preferences that should follow the user across devices belong in sync. Large caches, logs, and temporary data belong in local. Ephemeral session state that should survive service worker restarts but not browser restarts belongs in session. Misusing areas — like putting a 50KB cache in sync — leads to silent quota failures and data loss.

Design for change from the beginning. Extensions evolve, and the shape of stored data changes with them. Implement a schema version number and migration logic in onInstalled so that updates transform old data structures into new ones without losing user data. Users who have been running your extension for years should not lose their settings because you renamed a key.

Anti-Patterns

  • Reading storage on every keystroke or scroll eventchrome.storage is async and has I/O overhead; read state once on context initialization, cache it in memory, and use onChanged listeners to stay up to date rather than polling.

  • Storing large binary data in sync storage — the 100KB total and 8KB per-item limits are strict and silently enforced; large data belongs in local storage or IndexedDB.

  • Treating storage writes as synchronous — calling chrome.storage.local.set() without awaiting it and immediately reading the value back may return stale data, especially under concurrent writes from multiple contexts.

  • Not providing default values on reads — calling chrome.storage.local.get('key') without a default means the first-ever read returns undefined, which propagates through the UI as blank or broken state.

  • Skipping data migration on extension updates — changing the shape of stored data (renaming keys, restructuring objects) without a migration step leaves existing users with orphaned data and broken functionality.

Common Pitfalls

  • Exceeding chrome.storage.sync quotas (100 KB total, 8 KB per item) silently fails or throws — always keep synced data small and store large payloads in local instead.
  • Treating chrome.storage like synchronous localStorage — all operations are asynchronous and must be awaited, or state may be stale or lost.

Install this skill directly: skilldb add browser-extension-skills

Get CLI access →