Skip to content

Architecture: Offline & Synchronization

Strategy: Cache-First (PWA + React Query)

ChatKcal employs a "Lightweight" offline strategy that prioritizes UI responsiveness and "Recent Read + New Write" capabilities over full local database synchronization.


1. Core Concepts

The Two Layers of Offline

  1. The App Shell (PWA):

  2. Goal: Ensure the application starts without an internet connection.

  3. Mechanism: A Service Worker (via vite-plugin-pwa) caches static assets (index.html, JS bundles, CSS).
  4. Behavior: When offline, the browser serves these files from the Cache Storage API instead of failing.

  5. The Data Layer (React Query):

  6. Goal: Ensure the user sees data (meals) and can perform actions (log meals) without a connection.

  7. Mechanism: TanStack Query with persistQueryClient backed by idb-keyval (IndexedDB).

2. Read Strategy (Stale-While-Revalidate)

We treat the server as the "Source of Truth" but the local cache as the "Source of Speed."

  • Fetching: When a user views a date, we first check the local cache.
    • Hit (Fresh): If data is < 10 seconds old (staleTime), display immediately without refetching.
    • Hit (Stale): Display data immediately, then fetch fresh data from AppSync in the background.
    • Miss: Fetch from AppSync (or disk cache).
  • Retention (gcTime / maxAge):
    • Configuration: 30 Days for Persistence (maxAge), Infinity for In-Memory (gcTime).
    • Reasoning: To support the "Cold Cache" scenario (e.g., reviewing history on a flight). Inactive days are retained in IndexedDB for 30 days before being garbage collected. This allows for a robust offline history experience with minimal storage cost (\<5MB for typical usage).
    • Technical Note: We explicitly set the in-memory gcTime to Infinity because TanStack Query uses setTimeout for garbage collection, which overflows with values larger than ~24.8 days (2^31-1 ms), causing immediate cache eviction. maxAge handles the 30-day persistence expiry correctly using mathematical timestamp checks.

3. Write Strategy (Optimistic Updates)

We prioritize immediate user feedback.

  1. User Action: User logs a meal.
  2. Optimistic Update: We manually inject the new meal into the local React Query cache. The UI updates instantly.
  3. Network Request: The mutation is sent to the API.
  4. Offline Handling:

  5. If the network is unavailable, persistQueryClient pauses the mutation.

  6. The mutation is persisted to IndexedDB (separate from the data cache).
  7. Resume: When the browser detects the online event, the mutation is automatically retried.

  8. Rollback: If the server rejects the request (e.g., validation error), the optimistic update is reverted, and the user is notified.

4. Why not Amplify DataStore?

We explicitly chose React Query over AWS Amplify DataStore to:

  • Preserve Custom Logic: ChatKcal relies on custom AppSync JS resolvers for aggregation and logic. DataStore requires standard auto-generated VTL resolvers.
  • Avoid Schema Pollution: DataStore imposes strict fields (_version, _deleted) on the DynamoDB schema. React Query works with our clean, existing schema.
  • Reduce Complexity: We avoid the complexity of a background sync engine and conflict resolution protocol, as ChatKcal is primarily a single-user-per-view application.

5. Technical Implementation

  • Storage Engine: idb-keyval (Simple Promise-based wrapper for IndexedDB).
  • Persister: createSyncStoragePersister from @tanstack/query-sync-storage-persister.
  • Service Layer: frontend/src/services/ApiMealService.js encapsulates all GraphQL calls, keeping the React components focused on UI and State.

6. Timezone Handling Strategy

ChatKcal defines a "Day" dynamically based on the user's local device time (The "4 AM Rule"). This creates unique challenges for caching when users travel.

Strategy: "Sticky Date, Self-Correcting Boundary"

We key the cache purely by Logical Date (YYYY-MM-DD), ignoring the timezone offset.

  • The Scenario: A user travels from New York (UTC-5) to London (UTC+0).
  • Offline Behavior:
    • The user opens the app in London.
    • The Logical Date is still 2025-12-31.
    • Cache Hit: The app displays the data fetched in New York.
    • Benefit: The user sees their content immediately instead of a "Cold Cache" (blank screen), which would happen if we keyed by Timezone.
  • Online Reconciliation:
    • React Query triggers a background refetch.
    • The client calculates the new UTC range for 2025-12-31 based on London time.
    • The API returns the corrected list of meals for this new timeframe.
    • The UI updates to reflect the new "4 AM boundary" (e.g., a meal logged at 3 AM NY time might shift from "Today" to "Yesterday").

Limitation: Historical Auditing

We currently do not persist the user's Timezone string (e.g., "America/New_York") in the database.

  • Implication: We rely on the explicitly stored UserDate string for daily aggregation.
  • Constraint: If we ever decide to change the "4 AM Rule" (e.g., to 5 AM), we cannot accurately re-process historical data because we do not know the original timezone offset for each record's CreatedAt timestamp. This is a known trade-off accepted to avoid backend schema migrations during Phase 1.