How Data Synchronization Between localStorage and Supabase Works in TCG Pocket Collection Tracker

The app implements timestamp-based cache validation in frontend/src/services/collection/collectionService.ts to ensure the browser's localStorage stays in sync with Supabase by comparing collection_last_updated timestamps, writing to the remote database first, and refreshing the cache only after successful commits.

The tcg-pocket-collection-tracker repository solves the challenge of maintaining collection consistency across devices by implementing a hybrid caching strategy. This approach prioritizes data synchronization between localStorage and Supabase to deliver fast, offline-capable reads while guaranteeing that the remote PostgreSQL database remains the ultimate source of truth. The architecture ensures that users see the most recent data instantly without unnecessary network requests, while preventing stale cache scenarios through explicit timestamp validation.

The Core Synchronization Architecture

The synchronization logic centers on three primary helpers within the collection service module. These utilities manage the delicate balance between local performance and remote consistency.

Cache Schema and Storage Keys

The system stores two distinct keys per user in localStorage:

  • tcg_collection_cache_v2_<email> – A serialized representation of the user's collection rows
  • tcg_collection_timestamp_v2_<email> – An ISO-formatted timestamp marking the last successful server synchronization

This dual-key approach allows the application to validate cache freshness without deserializing the entire dataset. The timestamp acts as a version vector, enabling quick comparisons against the server's collection_last_updated field stored in the accounts table.

Timestamp-Based Cache Validation

When reading data, the service performs a chronological comparison rather than simple existence checks. If the cached timestamp is newer than or equal to the server's collection_last_updated value, the local data is considered authoritative. This pattern prevents race conditions where a slower Supabase fetch might overwrite newer local changes that haven't synced yet.

Reading Collection Data with Cache-First Strategy

The getCollection(email, collectionLastUpdatedRaw?) function orchestrates the read path, implementing a sophisticated fallback mechanism that prioritizes responsiveness.

Cache Hit Scenario

When the cached timestamp validates against the server record, the function immediately deserializes tcg_collection_cache_v2_<email> into a Map<internal_id, CollectionRow>. This path requires zero network calls, allowing the UI to render instantly even on unstable connections. The Map structure provides O(1) lookups for individual card amounts during subsequent interactions.

Cache Miss and Remote Fetch

If the cache is absent, corrupted, or chronologically behind the server timestamp, the system invokes fetchCollectionFromAPI. This helper pages through the Supabase tables (card_amounts and collection) using range queries:

// Conceptual implementation detail from collectionService.ts
const fetchCollectionFromAPI = async (email: string) => {
  // Paginated fetching using supabase.from(...).range()
  const { data, error } = await supabase
    .from('card_amounts')
    .select('*')
    .eq('user_email', email)
    .range(from, to);
  
  // After successful fetch, update local cache
  updateCollectionCache(email, data, new Date().toISOString());
}

Upon completing the remote fetch, the cache is immediately populated with the fresh dataset and current timestamp, optimizing subsequent reads.

Writing Collection Data and Maintaining Consistency

Write operations follow a strict Supabase-first pattern to prevent data loss and ensure cross-device consistency.

The Supabase-First Write Pattern

When users modify their collection via updateCards or deleteCard, the service executes the database mutations immediately:

  1. Upsert operations target the card_amounts and collection tables
  2. Metadata updates increment the accounts.collection_last_updated timestamp to signal global state changes
  3. Transaction integrity ensures all related tables update atomically before proceeding

Cache Rehydration After Successful Writes

Only after receiving a successful response from Supabase does the system update localStorage. The updateCards function reads the current cache (or fetches fresh data if missing), applies the modifications to the in-memory Map, and persists the result:

import { updateCards } from '@/services/collection/collectionService'

async function addCards(email: string, updates: CardAmountUpdate[]) {
  // Writes to Supabase, then updates cache automatically
  const { cards, account } = await updateCards(email, updates)
  console.log('Updated collection size:', cards.size)
}

Similarly, deleteCard removes the specific card ID from the cached row structure before rewriting to localStorage. This optimistic update pattern ensures the UI reflects changes instantly while maintaining durability guarantees.

Error Handling and Cache Invalidation

If any database operation fails, the system invokes removeLocalCacheItems to purge both storage keys. This aggressive invalidation prevents the application from displaying stale data on subsequent loads, forcing a fresh fetch from Supabase when connectivity restores.

Edge Cases and Resilience

The service implements defensive programming patterns to handle browser environment variations and storage limitations.

Handling Corrupted or Missing localStorage

All cache helpers first verify typeof localStorage === 'undefined' to support server-side rendering and private browsing modes. When getCollectionFromCache encounters JSON parse errors (indicating storage corruption), it catches the exception, clears the malformed entry, and returns null to trigger a network fetch. This self-healing mechanism prevents application crashes from malformed browser storage.

Storage Quota Management

The updateCollectionCache function wraps storage operations in try-catch blocks specifically targeting DOMException with QuotaExceededError. When the browser's storage limit is reached, the service removes the existing cache entry for that user and logs the fallback, allowing the application to continue functioning with degraded caching performance rather than failing catastrophically.

Summary

  • Timestamp validation in getCollection ensures localStorage only serves data that is newer or equal to the Supabase collection_last_updated record
  • Dual-key storage separates collection data (tcg_collection_cache_v2_<email>) from synchronization metadata (tcg_collection_timestamp_v2_<email>) for efficient validation
  • Supabase-first writes guarantee that updateCards and deleteCard persist changes to the remote database before updating the local cache, ensuring eventual consistency across devices
  • Automatic cache invalidation via removeLocalCacheItems prevents stale data display when network operations fail
  • Defensive error handling manages missing localStorage, corrupted JSON, and quota exceeded scenarios without breaking the user experience

Frequently Asked Questions

How does the app prevent stale cache data from overwriting newer server data?

The system compares the ISO timestamp stored in tcg_collection_timestamp_v2_<email> against the server's collection_last_updated field from the accounts table. The cache is only used when its timestamp is newer than or equal to the server timestamp. If the server has newer data, the cache is bypassed entirely and refreshed from Supabase using paginated range queries on the card_amounts table.

What happens if the Supabase write succeeds but the localStorage update fails?

According to the implementation in collectionService.ts, the local cache update occurs after the Supabase commit returns successfully. If the subsequent localStorage.setItem fails (for example, due to a QuotaExceededError), the updateCollectionCache function catches the DOMException, removes the old cache entry, and logs the error. The next read operation will detect the missing cache and fetch fresh data from Supabase, maintaining eventual consistency.

Does the app support offline usage with this synchronization strategy?

The architecture supports offline reads when valid cache exists, but writes require connectivity. Since the implementation checks typeof localStorage === 'undefined' before all cache operations, it gracefully degrades when Web Storage isn't available. However, the current Supabase-first write pattern means that user modifications during offline periods would fail at the API layer and trigger cache invalidation via removeLocalCacheItems. Future enhancements could implement a pending-changes queue to support true offline-first capabilities.

Which Supabase tables are involved in the collection synchronization?

The synchronization flow interacts with three primary tables defined in the Supabase schema: the accounts table (which stores the collection_last_updated metadata), the card_amounts table (containing individual card quantities), and the collection table (holding additional collection metadata). The getCollection function specifically pages through card_amounts or collection depending on the query context, while write operations update all relevant tables transactionally before refreshing the localStorage cache.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →