workout-app

Backend Setup

The iOS app is fully usable without a backend — it falls back to MockFoodResolver. Once you wire Supabase + Groq, food typing routes through the real resolver and per-user data syncs to the cloud.

Stack

Layer Choice Notes
Database Supabase Postgres Free tier; foods catalog + per-user logs all here.
Auth Supabase Auth (Sign in with Apple) Mandatory at first launch. RLS scopes user data by auth.uid().
API Supabase Edge Functions (Deno) Holds the Groq key server-side; iOS never sees it.
AI Groq (Llama 3.3 70B) Free tier; called only on foods cache miss.
Seed USDA FoodData Central + Open Food Facts One-shot Node import populates the canonical catalog.

iOS holds two values: SUPABASE_URL (project URL) + SUPABASE_ANON_KEY (designed to be embedded). Everything sensitive (Groq key, DB master password, service role key) stays on the server.

Prerequisites

Step 1 — Apply the schema migration

The migration creates the foods catalog, per-user log tables, and RLS policies.

# From the repo root
supabase login
supabase link --project-ref evsufhtdimexwghqkazy   # use your ref
supabase db push                                   # applies supabase/migrations/0001_init.sql

Alternatively (if you don’t want the CLI yet): open the Supabase dashboard → SQL Editor → paste the contents of supabase/migrations/0001_init.sql → Run. It’s idempotent.

Verify in Database → Tables:

Step 2 — Configure Edge Function secrets

The resolve-food Edge Function (deployed in Step 4) needs the Groq key and a service role key (to write to foods/food_aliases after AI resolution).

Get the service role key from Project Settings → API (labeled service_role secret). Treat it like a master password — never put it in any iOS file or commit it.

supabase secrets set \
  GROQ_API_KEY=gsk_yourkeyhere \
  SUPABASE_URL=https://evsufhtdimexwghqkazy.supabase.co \
  SUPABASE_SERVICE_ROLE_KEY=eyJh-service-role-key

These secrets only exist in the Edge Function runtime — never in the repo, never in the iOS bundle.

Step 3 — Seed the foods catalog

Run the Node seed script (lives in scripts/, written next iteration):

cd scripts
cp .env.example .env             # gitignored; fill with SERVICE_ROLE key
npm install
node seed-usda.mjs               # ~5 min: 1500 USDA Foundation Foods
node seed-open-food-facts.mjs    # ~30 min: ~50k branded items (optional)

The script normalizes every entry to per-100g and inserts both foods rows and common food_aliases (English; locale tags will be populated on later passes).

Step 4 — Deploy the resolve-food Edge Function

(Edge Function code lives in supabase/functions/resolve-food/, written next iteration.)

supabase functions deploy resolve-food

Test it without the iOS app:

curl -X POST \
  "https://evsufhtdimexwghqkazy.supabase.co/functions/v1/resolve-food" \
  -H "Authorization: Bearer <user-jwt>" \
  -H "Content-Type: application/json" \
  -d '{"input": "150g chicken breast"}'

Step 5 — Wire the iOS side

(iOS wiring lands in a follow-up iteration. For now MockFoodResolver is still active.)

  1. WorkoutApp/Resources/Secrets.xcconfig already has SUPABASE_URL + SUPABASE_ANON_KEY filled in.
  2. xcodegen generate
  3. Build — AppEnvironment.live() will register RemoteFoodResolver automatically.

Resolution flow (reference)

USER TYPES "150g chicken"
        │
        ▼
iOS parses quantity (150g) + name (chicken)
        │
        ▼
iOS calls supabase.functions.invoke('resolve-food', { input })
        │
        ▼
Edge Function:
  1. Search foods + food_aliases for "chicken"
       HIT  → scale per-100g macros to 150g, return
       MISS → POST to Groq with the resolver prompt
                  ↓
              Validate JSON, write canonical foods row + aliases + servings
                  ↓
              Return resolved entry
        │
        ▼
iOS writes food_log_entries row (RLS scopes to user)

food_aliases lets chicken, chicken breast, poulet all map to the same foods row. food_servings lets 1 cup broccoli resolve via the broccoli row + cup → grams lookup.

Apple Health policy

We push food/water/workout/weight logs to HealthKit via NSHealthUpdateUsageDescription. We never read from HealthKit — the user’s data lives in our DB, and HealthKit is just an output. (NSHealthShareUsageDescription will be removed in project.yml next iteration.)

Status indicator

Profile → About reads AppConfig.fromBundle() and shows: