Skip to content

Migrate storage from localStorage to IndexedDB using localForage#2102

Merged
RodriSanchez1 merged 6 commits intomasterfrom
migratePresistStorageToIndexedDb
Feb 11, 2026
Merged

Migrate storage from localStorage to IndexedDB using localForage#2102
RodriSanchez1 merged 6 commits intomasterfrom
migratePresistStorageToIndexedDb

Conversation

@RodriSanchez1
Copy link
Collaborator

@RodriSanchez1 RodriSanchez1 commented Feb 6, 2026

Storage Migration: localStorage to IndexedDB

Overview

This document describes the storage migration implemented in Cboard to move from localStorage to IndexedDB for persisting Redux state.

Problem

Users with many boards were encountering QuotaExceededError because localStorage has a strict ~5-10MB limit per origin. When users created many boards with images and tiles, they would exceed this limit and lose the ability to save new data.

Solution

Migrate to IndexedDB via localForage, which supports gigabytes of storage depending on the browser.

Aspect localStorage IndexedDB
Storage limit ~5-10MB Gigabytes (browser-dependent)
API Synchronous (blocks main thread) Asynchronous (non-blocking)
Data types Strings only Structured data, blobs, files
Browser support All browsers All modern browsers

Implementation

The migration is implemented in src/reducers.js using a storage wrapper pattern.

How It Works

┌─────────────────────────────────────────────────────────────────┐
│                     APP INITIALIZATION                          │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│  redux-persist calls getItem('persist:root')                    │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│  Check IndexedDB for 'persist:root'                             │
│  ┌─────────────────┐                                            │
│  │  Data found?    │─── YES ──▶ Return data (already migrated)  │
│  └─────────────────┘                                            │
│          │ NO                                                   │
│          ▼                                                      │
│  ┌─────────────────┐                                            │
│  │ Check localStorage│                                          │
│  └─────────────────┘                                            │
│          │                                                      │
│          ▼                                                      │
│  ┌─────────────────┐                                            │
│  │  Data found?    │─── NO ───▶ Return null (new user)          │
│  └─────────────────┘                                            │
│          │ YES                                                  │
│          ▼                                                      │
│  ┌─────────────────────────────────────────────┐                │
│  │ MIGRATION: Copy localStorage → IndexedDB    │                │
│  │ Log: "Migrating persist:root..."            │                │
│  └─────────────────────────────────────────────┘                │
│          │                                                      │
│          ▼                                                      │
│  Return data (migration complete)                               │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│  App loads with user's boards                                   │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│  User makes changes → setItem() → Saved to IndexedDB            │
│  (No more quota errors!)                                        │
└─────────────────────────────────────────────────────────────────┘

Storage Wrapper

The migration uses a custom storage wrapper that implements the redux-persist storage interface:

const createMigratingStorage = (oldStorage, newStorage) => ({
  async getItem(key) { ... },   // Read with migration fallback
  async setItem(key, value) { ... },  // Write to localForage
  async removeItem(key) { ... }  // Remove from localForage
});

getItem(key)

  1. Try to read from localForage (new storage)
  2. If not found, try localStorage (old storage)
  3. If found in localStorage, copy to localForage and remove from localStorage
  4. Return the data

setItem(key, value)

  1. Write to localForage (which internally uses IndexedDB, or falls back to localStorage if unavailable)

removeItem(key)

  1. Remove from localForage

Persisted Keys

The app persists two separate keys:

Key Description
persist:root Main app state (boards, communicator, app settings, etc.)
persist:language Language settings (current language, available languages)

Both keys are migrated automatically by the storage wrapper.

Edge Cases Handled

Safari Private Browsing Mode

IndexedDB is not persistent in Safari private mode. localForage automatically detects this and uses localStorage as its driver.

New Users

For new users (no data in either storage), getItem returns null and redux-persist initializes with default state.

Already Migrated Users

On subsequent visits, data is found in IndexedDB immediately. localStorage is never checked, so there's no performance overhead.

Cordova/Mobile App

localForage works in Cordova environments and automatically selects the best available storage backend.

Multi-Tab Usage

IndexedDB is designed for multi-tab access. Data is consistent across tabs.

Verifying the Migration

Check IndexedDB in DevTools

  1. Open Chrome DevTools
  2. Go to Application tab
  3. Expand IndexedDB in the sidebar
  4. Look for cboardcboard_store
  5. You should see persist:root and persist:language keys

Console Messages

During migration, you'll see these messages in the console:

Cboard: Migrating persist:root from localStorage to IndexedDB...
Cboard: Successfully migrated persist:root
Cboard: Migrating persist:language from localStorage to IndexedDB...
Cboard: Successfully migrated persist:language
Cboard: Cleaned up persist:root from localStorage
Cboard: Migrating persist:language from localStorage to IndexedDB...
Cboard: Successfully migrated persist:language
Cboard: Cleaned up persist:language from localStorage

After migration, these messages won't appear on subsequent page loads.

Rollback

If issues are discovered, revert to localStorage by changing the storage config in src/reducers.js:

const config = {
  key: 'root',
  storage: storage,  // Change from migratingStorage to storage
  // ...
};

const languagePersistConfig = {
  key: 'language',
  storage: storage,  // Change from migratingStorage to storage
  // ...
};

Note: After migration, old localStorage data is cleaned up. Users who have already migrated will have their data in localForage (IndexedDB) only.

Dependencies

  • localForage - IndexedDB wrapper with localStorage-like API

References

Related Issues

close #1514

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR migrates Redux persisted state storage from localStorage to IndexedDB by switching redux-persist’s storage engine to localForage, while providing a wrapper that can transparently migrate existing persisted keys from the legacy storage on first read.

Changes:

  • Add localforage dependency and lockfile entries.
  • Configure localForage to use a cboard IndexedDB store.
  • Replace redux-persist storage with a migrating storage wrapper that reads from IndexedDB first and migrates from legacy localStorage when needed.

Reviewed changes

Copilot reviewed 2 out of 3 changed files in this pull request and generated 4 comments.

File Description
package.json Adds localforage dependency required for IndexedDB-backed persistence.
yarn.lock Locks localforage and its transitive dependency versions.
src/reducers.js Introduces migrating storage wrapper and switches redux-persist configs to use it.

src/reducers.js Outdated
Comment on lines 32 to 62
const createMigratingStorage = (oldStorage, newStorage) => ({
/**
* Retrieves a value from storage, migrating from old to new if necessary.
* Called by redux-persist on app initialization.
*/
async getItem(key) {
try {
const newValue = await newStorage.getItem(key);
if (newValue !== null && newValue !== undefined) {
return newValue;
}
} catch (err) {
console.warn('Cboard: IndexedDB read failed', err);
}

try {
const oldValue = await oldStorage.getItem(key);
if (oldValue !== null && oldValue !== undefined) {
console.log(
`Cboard: Migrating ${key} from localStorage to IndexedDB...`
);
try {
await newStorage.setItem(key, oldValue);
console.log(`Cboard: Successfully migrated ${key}`);
await oldStorage.removeItem(key);
console.log(`Cboard: Cleaned up ${key} from localStorage`);
} catch (writeErr) {
console.warn('Cboard: Migration write failed', writeErr);
}
return oldValue;
}
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new migrating storage wrapper introduces important behavior (read-from-new, fallback-to-old, one-time copy + cleanup). There are currently only smoke tests for createReducer, so this migration path isn’t covered. Add Jest tests that verify (1) when only legacy storage has persist:*, getItem copies to localForage and removes from legacy storage, and (2) when localForage already has the key, legacy storage is not read/modified.

Copilot generated this review using guidance from repository custom instructions.
@RodriSanchez1 RodriSanchez1 merged commit 370e110 into master Feb 11, 2026
4 checks passed
@RodriSanchez1 RodriSanchez1 deleted the migratePresistStorageToIndexedDb branch February 11, 2026 17:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Exceeding storage quota

1 participant