ADR 001: Flexible Nutrient Schema
Metadata
- Status: Accepted & Implemented
- Date: 2026-01-06
- Type: Architecture Decision Record (ADR)
- Driver: Advanced Nutrient Tracking (Fiber, Sodium)
Summary
This document outlines the architectural decision to support user-defined extended nutrients (Fiber, Sodium, etc.) using a Metadata-Driven DynamoDB Map strategy. It balances schema flexibility with data integrity, performance, and cost efficiency.
1. Problem Statement
Current Limitation
The current schema hardcodes calories, protein, carbs, and fat as top-level attributes. Adding new nutrients requires:
- Schema migrations (DynamoDB & GraphQL).
- Updates to aggregation logic (
addMealresolver). - Frontend type changes.
Goal: Support arbitrary nutrients (e.g., Fiber, Sodium, Sugar) without schema changes.
2. Solution Options
Add fiber, sodium as explicit columns.
Pros: Simple query/indexing.
Cons: Rigid. Requires migration for every new nutrient.
Store nutrients: String (JSON).
Pros: Maximum flexibility.
Cons: No Server-Side Aggregation. Client must fetch all meals to calc totals. Breaks "Day Summary" architecture.
Store nutrients: Map<String, Number>.
Pros:
- Flexible keys.
- Supports Atomic Aggregation via
SET path = path + val.
Verdict: Best balance of flexibility and performance.
3. The Metadata-Driven Strategy
To solve the "Black Box" problem of dynamic keys (where AWSJSON offers no type safety), we will use the CONFIG#TARGETS item as a Schema Registry.
User Settings Schema (CONFIG#TARGETS)
We will extend the existing Configuration item to store a definition for every nutrient the user wants to track. We use a unified Registry Map to preserve metadata (units) even for archived items.
{
"nutrientRegistry": {
"fiber": {
"unit": "g",
"label": "Fiber",
"target": 30,
"status": "ACTIVE"
},
"sodium": {
"unit": "mg",
"label": "Sodium",
"target": 2300,
"status": "ACTIVE"
},
"sugar": {
"unit": "g",
"label": "Sugar",
"target": null,
"status": "ARCHIVED"
}
},
"version": 6
}
Schema Improvements
- Unit Persistence:
ARCHIVEDitems retain their unit definitions, ensuring safe reactivation without data corruption. - Optional Targets: Users can track a nutrient without setting a goal (target can be
null). - Single Source: No need to sync separate
activevshistoricallists.
Cost & Performance Justification
This decision is driven by Resource Efficiency:
- Minimize Get Requests: Combining targets, registry, and history into one row reduces the total number of DynamoDB
GetItemcalls. - Stay Within Free Tier: By reducing RCU/WCU consumption, we ensure the application remains cost-effective and stays within AWS Free Tier limits longer.
4. Aggregation & Technical Implementation
To maintain real-time Day Summaries, we update the DaySummary item atomically when adding a meal.
Dynamic Update Logic
The AppSync Resolver (JS) will dynamically construct the UpdateItem expression.
// Input: { fiber: 5, sodium: 200 }
// Output Expression:
SET
nutrients.fiber = if_not_exists(nutrients.fiber,: zero) +: fiberVal,
nutrients.sodium = if_not_exists(nutrients.sodium,: zero) +: sodiumVal
Performance Optimization: "Read-Before-Write"
To avoid doubling the RCU cost of every addMeal call (fetching the Registry to validate keys), we use Frontend-Assisted Validation.
- Mechanism: The frontend passes a
registrySnapshot(list of active keys derived fromnutrientRegistry) along with theaddMealmutation. - Security: The AppSync resolver uses this list to filter the
extendedNutrientsmap. While the frontend could be tampered with, it provides a sufficient guard against AI hallucinations without requiring an extra database hit. - Gatekeeping: The resolver enforces Reserved Keyword filtering (blocking
PK,SK,type) regardless of the snapshot.
Precision Strategy
We will enforce rounding to 2 decimal places in the AppSync Resolver.
- Reasoning: While frontend formatting hides artifacts, accumulating floating point drift (e.g.
10.1 + 20.2 = 30.299999999999997) in theDaySummaryaggregation over thousands of meals can eventually cause issues. - Implementation: The resolver performs
Math.round(val * 100) / 100before writing to DynamoDB.
5. Math Integrity (Deletion)
The Math Ghost
If a user logs Sodium, then disables Sodium tracking, and then deletes that old meal, we must still subtract the Sodium from the DaySummary.
Rule: The deleteMeal resolver ignores current settings and subtracts based on the values stored on the meal record itself.
6. Prompt Engineering
We need to inject the user's active nutrient list into the prompt dynamically.
Dynamic Injection:
We will append a specific instruction block to the System Prompt based on the user's activeNutrients:
"You must also estimate the following additional nutrients: Fiber (unit: g), Sodium (unit: mg). Return these inside an
extendedNutrientsobject in the JSON."
Response Format: We will separate core macros from extended nutrients to preserve backward compatibility.
Strict Numeric Return
The LLM must be instructed to return only the number (e.g., 200) for extended nutrients, not the string with units (e.g., "200mg"). The prompt will explicitly state: "CRITICAL: The values must be numbers ONLY. Do not include the unit string."
{
"mealSummary": "...",
"calories": 500,
"macros": {
"protein": 30,
"carbs": 50,
"fat": 10
},
"extendedNutrients": {
"fiber": 5,
"sodium": 200
}
}
7. Lifecycle & Data Integrity
Rule: Once a nutrient is created in the Registry, its Unit is permanent.
- Change Path: If a user wants to switch from
mgtog, they must disable the old nutrient and create a new one (e.g., "Sodium_G"). - Reasoning: Prevents "mathematical radioactivity" where a single Daily Total contains mixed units.
Rule: The CONFIG#TARGETS item will use a version attribute for updates.
- Mechanism: Every update must include the
expectedVersion. - Conflict: If two devices update settings simultaneously, the second one will fail. The user must refresh their settings state and re-apply their change.
- Result: Prevents the "God Row" from being corrupted by stale device states.
Constraint: This data is Report Only.
- Limit: Dynamic nutrients cannot be indexed or searched (e.g., "Find all meals > 500mg Sodium") without a full table scan.
- Verdict: Accepted tradeoff for schema flexibility and cost efficiency.
8. Proposed Schema Changes
type Meal {
# ... existing fields
extendedNutrients: AWSJSON
# Serialized JSON string: "{\"fiber\": 5}"
}
type DaySummary {
# ... existing fields
extendedNutrients: AWSJSON
}
9. Decision Record
Final Decision
Proceed with Metadata-Driven Maps + Frontend-Assisted Validation + Optimistic Locking.
- Storage: Extended
CONFIG#TARGETSitem withversioningand unifiednutrientRegistry. - API:
registrySnapshotpassed inaddMealto avoid extra RCU. - Logic: Unit locking enforced at the UI/Registry level.