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
ApiMealService: Standard AppSync implementation (refactored to takeclientin constructor).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.
5.2 Sync Logic (The usePromotion Hook)
- Detection: Check
ChatKcalGuestDBfor data. - User Choice: Prompt: "Sync local data?"
- Batch Upload:
- Serialize local meals and call
importGuestHistory(chunked if > 25 items). - Idempotency: The Lambda uses
BatchWriteItemand ignores duplicate Keys (client-generated UUIDs).
- Serialize local meals and call
- 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,_deletedfields to our clean DynamoDB table. - Logic Blackbox: Incompatible with our custom AppSync JS resolvers (business logic).
- Schema Pollution: Requires adding
8. Impacts & Risks
- Cache Poisoning: When switching from
LocalMealServicetoApiMealService, we MUST callqueryClient.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 (uuidv4) 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
- Refactor Types: Create
IMealServiceandISettingsService. - Refactor ApiServices: Update existing services to implement interfaces.
- Install Dexie: Add
dexiedependency. - Implement LocalServices: Build the IndexedDB logic.
- Backend Implementation: Create
importGuestHistoryLambda and AppSync Resolver. - Create ServiceProvider: Wrap the app and handle
queryClient.resetQueries(). - Update Hooks: Update
useMealsanduseSettingsto use the injected services.