Calorie tracker data model
Purpose
This model is designed for a trustworthy, low-friction calorie and nutrient tracker. It supports packaged foods, whole foods, household recipes, common dishes, saved meals, quantity ranges, barcode/search logging, repeat logging, and AI-assisted drafts without allowing estimates to masquerade as exact data.
The database is PostgreSQL hosted by Supabase. Flutter may read and write user-owned CRUD data through Row Level Security (RLS). Privileged imports, AI processing, and computed operations run through trusted server code.
Design rules
- A historical log never changes when catalog data changes. Every confirmed log stores a nutrient snapshot.
- Precision is explicit. Estimated quantities and nutrients carry minimum, expected, and maximum values.
- Published nutrition records are immutable. Corrections create a new item version.
- Meals, recipes, and foods are distinct. Foods and recipes are loggable nutrition items; saved meals are reusable collections of those items.
- Public and user-owned data share behavior, not permissions. Ownership and visibility are explicit on every relevant row.
- All external data is traceable. Imported records retain source, source identifier, version, and timestamps.
- AI creates drafts only. A user confirms or edits a draft before it becomes a log.
- Canonical units are boring. Mass uses grams, volume uses millilitres, energy uses kcal, and each nutrient has one canonical unit.
- User-local dates are stable. Logs retain the IANA timezone and local calendar date that applied when consumed.
- Retries are safe. Client-created records use UUIDs and idempotency keys.
Domain boundaries
- Identity and preferences: profile, locale, units, onboarding answers, notification settings.
- Goals: body measurements, active goal, and effective-dated calorie/macro targets.
- Nutrition catalog: food/recipe records, versions, nutrients, servings, barcodes, aliases, and provenance.
- Recipe composition: ingredients, cooking method, yield, and retention assumptions.
- Saved meals: reusable user-defined combinations.
- Logging: meal events, logged items, nutrient snapshots, and corrections.
- Assisted logging: voice/photo/text drafts and candidates.
- Personalization: favorites, derived patterns, suggestions, and notification outcomes.
Core tables
All primary keys are UUIDs unless noted. Every mutable table has created_at and updated_at. User-deletable synced tables also have deleted_at as a tombstone.
Identity and onboarding
profiles
One row per auth.users row.
user_id— primary key and foreign key toauth.users.iddisplay_namebirth_date— optional; do not store a changing integer agecountry_codelocale— BCP 47 language tagtimezone— IANA timezone namemass_unit—kgorlbheight_unit—cm,ft_inenergy_unit— initiallykcalonboarding_completed_at
user_preferences
user_id— primary keyprimary_focus— calorie tracking, macro tracking, meal planning, exercise, or habitsmeal_tracking_enabledmeal_planning_frequencydietary_preferences— validated JSON array until product choices stabilizeallergens— validated JSON arraynotification_settings— validated JSON with independently controllable reminder types
Use join tables instead of JSON when a value becomes searchable or gains business behavior.
user_goal_selections
Supports up to three onboarding goals without embedding an array in profiles.
user_idgoal_codepriority- unique
(user_id, goal_code)and(user_id, priority) - constraint limiting priority to 1–3
Goals and measurements
body_measurements
Append-only measurement history.
user_idmeasured_atweight_kg- optional
body_fat_percent,waist_cm source— manual, Apple Health, device, import
nutrition_goals
Effective-dated user intent.
user_idgoal_type— maintain, lose, gaintarget_weight_kgweekly_change_kgstarts_on,ends_onstatus— draft, active, completed, cancelled
Only one active goal may overlap a given date.
daily_targets
Versioned targets so historical reports use the target that was active that day.
user_ideffective_from,effective_toenergy_kcalprotein_g,carbohydrate_g,fat_g- optional fiber and micronutrient targets
calculation_method— formula identifier or manualcalculation_inputs— validated structured snapshotreview_status— generated, user-adjusted, professionally-reviewed
Generated targets must pass safety limits before activation. The calculation service records the formula version and never silently changes an active target. A user may have no target row at all; this is observe mode, not a zero-calorie target.
Data provenance
data_sources
code— USDA, OFF, NIN, user, FitGeniename,homepage_urllicense_name,license_url,attribution_textredistribution_allowed
import_runs
Server-only operational record.
source_idsource_releasestatusstarted_at,finished_at- counts for read, inserted, updated, rejected
error_summary
Rejected rows go to protected import diagnostics, not into the catalog.
Nutrient dictionary
nutrients
code— stable internal code such asenergy_kcal,protein_g,vitamin_c_mgnamecanonical_unitdisplay_orderis_core- optional USDA/NIN identifiers
Canonical units never change after use. Source-specific units are converted at ingestion boundaries.
Nutrition items
nutrition_items
Represents an atomic food, common dish, or recipe.
owner_user_id— null for public catalog itemskind—food,common_dish,recipevisibility— public, private, householdname, optionalbrand_namedescriptiondefault_localecurrent_version_idstatus— draft, active, archived
RLS treats owner_user_id IS NULL as catalog data, not globally writable data.
nutrition_item_versions
Published versions are append-only.
nutrition_item_idversion_numberbasis_amount— normally 100basis_unit—gormldensity_g_per_ml— nullableyield_grams_min,yield_grams_expected,yield_grams_max— recipes onlyconfidence— verified, label, calculated, estimatedsource_id,source_record_id,source_releasesource_updated_at,imported_atpublished_at,superseded_at- unique
(nutrition_item_id, version_number)
nutrition_values
One row per nutrient and item version.
item_version_idnutrient_idamount_min,amount_expected,amount_max- unique
(item_version_id, nutrient_id)
Amounts are per the version's basis. Constraints enforce 0 <= min <= expected <= max. Exact label values use the same number for all three fields.
serving_options
item_version_idlabel— cup, katori, roti, slice, packagequantity_value— usually 1quantity_unit— count, g, ml, cup, tbsp, etc.grams_min,grams_expected,grams_maxis_defaultsource— label, measured, household, estimated
A serving must either resolve to grams or directly resolve to a known item basis. UI labels are localized separately from stable unit codes.
barcodes
barcode_type— EAN-8, EAN-13, UPC-A, etc.normalized_valueitem_version_idmarket_country_codesource_id
Index normalized barcode lookup. Do not assume a barcode identifies the same formulation in every market.
item_aliases
nutrition_item_idlocalealiasalias_type— synonym, transliteration, regional name, brand
Search combines names, aliases, brands, recency, user ownership, and source quality. PostgreSQL full-text search plus trigram indexes are sufficient initially.
Recipes and cooking
recipe_details
One-to-one with a recipe item version.
recipe_version_idservings_expectedpreparation_methodcooking_method_codeinstructionscalculation_version
recipe_ingredients
recipe_version_idpositioningredient_item_version_idquantity_value,quantity_unitgrams_min,grams_expected,grams_maxretention_profile_id— nullablenotes
A database/service validation prevents recipe cycles. Recalculating a recipe creates a new recipe version rather than rewriting a published version.
retention_profiles and retention_factors
- Profile identifies source, release, food group, cooking method, and assumptions.
- Factor identifies nutrient and retained fraction.
- Fractions are validated and versioned.
Retention math must be covered by fixture-based tests and reviewed before it becomes user-facing. It is an estimate, so calculated recipe outputs retain ranges and provenance.
Saved meals
meal_templates
user_idname- optional
default_meal_slot last_used_at
meal_template_items
meal_template_idpositionitem_version_idserving_option_id— nullablequantity- default gram range snapshot
Logging a template creates fresh log rows in one transaction. It does not point reports directly at mutable template rows.
Confirmed logs
meal_logs
Header for one eating event.
user_idconsumed_at— timestamp with timezoneconsumed_timezone— IANA name at entry timelocal_datemeal_slot— breakfast, lunch, dinner, snack, otherentry_method— search, barcode, repeat, template, voice, photo, manualnoteclient_idempotency_keystatus— draft, active, deleted- unique
(user_id, client_idempotency_key)when present
Clients may assemble items and nutrient snapshots only while a meal is a draft. finalize_meal_log validates ownership, requires at least one item and nutrient snapshots for every item, then activates the meal. Active or deleted history rejects direct child inserts and all direct mutation.
logged_items
meal_log_idsource_item_version_id— nullable only for an unreconciled manual itemsource_serving_option_id— nullabledisplay_name_snapshotquantity,quantity_unitgrams_min,grams_expected,grams_maxconfidenceposition
logged_nutrients
Immutable nutrition snapshot used for totals and reports.
logged_item_idnutrient_idamount_min,amount_expected,amount_max- unique
(logged_item_id, nutrient_id)
Daily totals are derived from logged_nutrients. A cache may be introduced only after measurement shows aggregation is too slow; snapshot rows remain the source of truth.
log_change_events
Append-only audit trail for corrections and deletion.
meal_log_iduser_idevent_typeoccurred_at- before/after references or a validated minimal diff
The normal edit transaction validates ownership, updates the current user-visible log, and appends an event. Audit events are deleted with their owning account so they cannot block the promised account-deletion path.
Assisted logging drafts
logging_drafts
user_idinput_type— voice, text, photostatus— processing, needs_review, confirmed, failed, expiredstorage_object_path— private bucket path, nullabletranscript— nullable and governed by retention policyparser_strategy— standard, enhanced, photoparser_version- nullable
provider,model,model_versionfor provider-backed strategies original_locale,detected_localeexpires_aterror_code,user_safe_error
draft_items
logging_draft_id- candidate item/version reference
- recognized text
- proposed quantity and gram range
- confidence
- match reason
- position
Confirmation uses a server transaction to create meal_logs, logged_items, and logged_nutrients. Provider output is validated before it reaches these tables. Raw model responses are not trusted domain records. NLP drafts identify food and quantity; deterministic nutrition calculations produce calorie and nutrient snapshots.
user_entitlements
Server-managed capabilities keep subscription decisions out of Flutter authorization logic.
user_idcapability— standard text parse, enhanced text parse, voice, photostatus— active, expired, revokedsource— subscription provider, promotion, support overrideeffective_from,effective_tosource_reference- unique active capability per user
Feature flags can disable a provider or enhanced parsing globally. Standard text parsing remains functional when enhanced parsing is removed, replaced, unavailable, or no longer entitled.
See docs/free-text-meal-parsing.md for the parser contract and failure policy.
Personalization
user_favorites
user_id,nutrition_item_id- unique pair
meal_patterns
Derived, replaceable data—not authoritative history.
user_id- day-of-week and meal slot
- candidate template or item
- support count, confidence
- computed_at, expires_at
suggestion_events
user_id, pattern referencechannel— in_app, pushshown_at,acted_at,dismissed_at- outcome
This allows recommendation usefulness and notification fatigue to be measured.
Important transactions and RPCs
Use database functions or trusted Node endpoints where multiple writes must succeed together:
finalize_meal_log(meal_log_id)confirm_logging_draft(draft_id, edits, idempotency_key)log_meal_template(template_id, consumed_at, quantity_overrides, idempotency_key)repeat_meal_log(source_meal_log_id, consumed_at, quantity_overrides, idempotency_key)publish_recipe_version(recipe_id, calculation_inputs)activate_daily_target(target_id)
Every function checks auth.uid()/server identity, validates all ranges and references, and returns a complete immutable result rather than requiring follow-up mutation.
RLS policy matrix
- Public catalog items and published versions: authenticated read; trusted importer write.
- Private and household items: owner/household read; owner write.
- Profiles, goals, measurements, templates, logs, favorites, drafts, and suggestions: owning user only.
- Import runs, provider diagnostics, retention administration: server roles only.
- Storage photos/audio: private bucket, owner path policy, short-lived signed URLs.
- Form/media catalog: public or signed read; trusted publisher write.
RLS tests must prove both allowed access and cross-user denial for every table and RPC. The Supabase service-role key never ships in Flutter.
Integrity and indexing
Required constraints and indexes include:
- Range ordering for quantities and nutrients.
- Positive serving quantities and recipe yields.
- Unique current version and one default serving per item version.
- Foreign keys that prevent deleting referenced published versions.
- User/date index on
meal_logs (user_id, local_date, consumed_at). - Barcode index on normalized value and market.
- Trigram/full-text indexes for item names, aliases, and brands.
- Template and draft ownership indexes.
- Partial indexes excluding tombstoned rows.
Macro-derived energy (4p + 4c + 9f) is a quality warning, not a hard constraint, because fiber, alcohol, organic acids, and source labeling rules create legitimate differences.
Validation and test fixtures
Before UI work depends on the model, automated tests cover:
- Exact packaged-food serving conversion.
- Gram, volume, count, fractional serving, katori, and roti quantities.
- Recipe scaling, cooked yield, and nutrient retention ranges.
- Copying a meal while changing only selected quantities.
- Catalog version updates leaving historical logs unchanged.
- Day boundaries and daylight-saving transitions.
- Duplicate retries producing one log.
- Cross-user RLS denial.
- Draft confirmation and provider failure recovery.
- Account deletion removing or anonymizing all private data according to policy.
Explicit non-goals for the first tracker release
- Social feeds and creator marketplace.
- Commerce.
- Medical diagnosis or treatment recommendations.
- Automatic logging without user confirmation.
- Premature daily-summary tables, event buses, or microservices.