Skip to content

Architecture Proposal: Guest Mode Architecture

Metadata

  • Status: Draft
  • Date: 2026-01-17
  • Related Issue: 2512_genai_food_tracking-kzq
  • Type: Architecture Change Proposal (ACP-001)

1. Overview

Guest Mode allows users to track meals and configure settings without an AWS Cognito account. Data is stored locally in the browser using IndexedDB. When a user eventually signs up, their local data is "promoted" (synced) to the cloud.

2. Service Abstraction (The Repository Pattern)

Currently, the frontend calls ApiMealService directly, passing an AmplifyClient. We will refactor this into a decoupled service layer.

2.1 Interface Definitions

// frontend/src/types/services.ts

export interface IMealService {
  getMeals(currentDate: Date): Promise<Meal[]>;
  addMeal(input: MealInput): Promise<MutationResult>;
  deleteMeal(input: DeleteMealInput): Promise<MutationResult>;
}

export interface ISettingsService {
  getUserTargets(): Promise<UserTargets>;
  updateUserTargets(targets: UserTargets): Promise<UserTargets>;
}

2.2 Implementations

  1. ApiMealService: Standard AppSync implementation (refactored to take client in constructor).
  2. LocalMealService: New implementation using Dexie.js to interface with IndexedDB.

3. Local Storage Schema (Dexie.js)

Database Name: ChatKcalGuestDB

Store Primary Key Indexes Description
meals sk userDate, createdAt Stores meal records. sk will be a client-side UUID.
settings key - Key-value store for user targets and preferences.

4. Service Injection

We will use a ServiceProvider to inject the correct implementation based on authentication state.

// frontend/src/providers/ServiceProvider.tsx

export function ServiceProvider({ children }) {
  const { user } = useAuthenticator();
  const apiClient = generateClient(); // Amplify

  const services = useMemo(() => {
    if (user) {
      return {
        mealService: new ApiMealService(apiClient),
        settingsService: new ApiSettingsService(apiClient)
      };
    } else {
      return {
        mealService: new LocalMealService(),
        settingsService: new LocalSettingsService()
      };
    }
  }, [user]);

  return (
    <ServiceContext.Provider value={services}>
      {children}
    </ServiceContext.Provider>
  );
}

5. Data Promotion (Guest -> User)

When user transitions from null to Authenticated, we must perform an atomic and idempotent migration.

5.1 The importGuestHistory Mutation

Instead of naively iterating addMeal (N+1 problem), we will implement a batch mutation backed by a Lambda Resolver.

type Mutation {
  importGuestHistory(meals: [String]!, settings: String): ImportResult
}

5.2 Sync Logic (The usePromotion Hook)

  1. Detection: Check ChatKcalGuestDB for data.
  2. User Choice: Prompt: "Sync local data?"
  3. Batch Upload:
    • Serialize local meals and call importGuestHistory (chunked if > 25 items).
    • Idempotency: The Lambda uses BatchWriteItem and ignores duplicate Keys (client-generated UUIDs).
  4. Cleanup: Only upon success response, clear ChatKcalGuestDB.

6. Conflict Resolution (Smart Merge)

If a user logs into an existing account that already has data:

  • Disjoint Data: New guest meals (unique UUIDs) are appended.
  • Overlapping Data: We adopt a "Trust the Guest" policy. If the user just logged a meal locally, that interaction is the most recent intent. The Lambda will overwrite the server copy for that specific ID.

7. Alternatives Considered

We evaluated and rejected two other strategies (detailed in ACP-002):

Option A: localStorage

  • Idea: Store meals as a JSON string in localStorage.
  • Reason for Rejection:
    • Blocking I/O: Serialization blocks the main thread, causing UI jank as history grows.
    • Capacity: Hard 5MB limit risks data loss for engaged users.

Option B: Amplify DataStore

  • Idea: Use AWS's "batteries-included" offline sync engine.
  • Reason for Rejection:
    • Schema Pollution: Requires adding _version, _deleted fields to our clean DynamoDB table.
    • Logic Blackbox: Incompatible with our custom AppSync JS resolvers (business logic).

8. Impacts & Risks

  • Cache Poisoning: When switching from LocalMealService to ApiMealService, we MUST call queryClient.resetQueries() to prevent showing stale guest data as user data.
  • UUID Generation: Local meals must generate strict MEAL#<ISO>#<UUID> keys using a robust library (uuid v4) to ensure global uniqueness during the merge.

9. Senior Critique & Edge Case Handling

9.1 The "Service Swap" Race Condition

Problem: When the user authenticates, ServiceProvider immediately swaps LocalMealService for ApiMealService. If usePromotion relies on the generic IMealService, it will attempt to read "Guest Data" from the API (where it doesn't exist yet). Solution: The usePromotion hook must bypass the Service Abstraction. It will import ChatKcalGuestDB (Dexie instance) directly to read the source data, while using ApiMealService (from context) to write the target data.

9.2 The "4 AM Rule" Parity

Problem: The Backend uses complex logic to determine "User Day" (4 AM to 4 AM). If LocalMealService naively filters by calendar date, the UI will show different meals when switching between Offline/Online modes. Solution: LocalMealService must explicitly utilize the shared src/utils/dateUtils (getUtcRange, getLogicalDate) to ensure its query logic creates the exact same time boundaries as the AppSync resolvers.

9.3 Validation at the Source

Problem: IndexedDB has no schema enforcement. If a guest writes invalid data (e.g., negative calories) that the API later rejects, the Sync will fail permanently. Solution: We must apply Zod-based validation before writing to Dexie. The UI must prevent invalid data from ever entering the "Guest" stage.

10. Execution Steps

  1. Refactor Types: Create IMealService and ISettingsService.
  2. Refactor ApiServices: Update existing services to implement interfaces.
  3. Install Dexie: Add dexie dependency.
  4. Implement LocalServices: Build the IndexedDB logic.
  5. Backend Implementation: Create importGuestHistory Lambda and AppSync Resolver.
  6. Create ServiceProvider: Wrap the app and handle queryClient.resetQueries().
  7. Update Hooks: Update useMeals and useSettings to use the injected services.