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

> Learn how localStorage and Supabase sync data in the TCG Pocket Collection Tracker. Explore timestamp validation and effective caching strategies for consistent data.

- Repository: [Marcel Panse/tcg-pocket-collection-tracker](https://github.com/marcelpanse/tcg-pocket-collection-tracker)
- Tags: internals
- Published: 2026-03-06

---

**The app implements timestamp-based cache validation in [`frontend/src/services/collection/collectionService.ts`](https://github.com/marcelpanse/tcg-pocket-collection-tracker/blob/main/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:

```typescript
// 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:

```typescript
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`](https://github.com/marcelpanse/tcg-pocket-collection-tracker/blob/main/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.