Next.js + Supabase + Gemini scaffold covering four features:
- Meals — photo upload → Gemini vision estimates calories/macros (
app/meals,app/api/meals/analyze) - Workouts — deterministic set logging + progression rule, no model calls (
app/workouts,app/api/workouts) - Form check — short clip → client-side frame extraction → Gemini feedback (
app/form-check,app/api/form/analyze) - Habits — daily metric logging + Recharts trend (
app/habits,app/api/habits)
npm install
cp .env.local.example .env.local # fill in Supabase + Gemini keysIn your Supabase project:
- Run
supabase/schema.sqlin the SQL editor (creates tables + RLS policies). - Create two private Storage buckets:
meal-imagesandform-clips. - Add storage policies on each bucket restricting reads/writes to paths prefixed with the requesting user's
auth.uid()— the API routes already upload to${user.id}/..., so a policy likebucket_id = 'meal-images' AND (storage.foldername(name))[1] = auth.uid()::textcovers it. - Enable email auth (or your provider of choice) under Authentication.
npm run devAuth UI isn't wired up yet — add a Supabase Auth flow (magic link or OAuth) before these routes are useful, since every route calls supabase.auth.getUser() and 401s without a session.
- RLS from day one (
supabase/schema.sql) — every user-data table has policies scoped toauth.uid(). Retrofitting this after real user data exists is painful and risky; it's cheap now. - Usage metering before every Gemini call (
lib/usage.ts) —meals/analyzeandform/analyzeboth check a daily cap first. Without this, one user hammering uploads can run up unbounded API cost. - Prompt versioning (
PROMPT_VERSIONSinlib/gemini.ts) — stored alongside every Gemini response ingemini_prompt_version. When you tweak a prompt and outputs get weird, you can tell which version produced which row. - Raw vs. corrected data kept separate (
meals.gemini_responsevsmeals.corrected) — never overwrite the model's original output with a user's correction. That pairing is exactly the data you'd want later to evaluate or fine-tune against. - Frame extraction happens client-side (
app/form-check/page.tsx) — the browser samples ~6 JPEG frames from the clip before anything is uploaded, so payload size and per-call Gemini cost stay bounded regardless of clip length, and raw video never has to be stored or sent to the model. - Workout progression is a plain function, not a model call (
suggestNextLoadinapp/api/workouts/route.ts) — progressive overload is deterministic; save the LLM budget for meals/form where vision is actually needed.
- Auth pages / session handling (sign-in, sign-out, middleware to protect routes)
- Editing/correcting a meal's estimated macros (the
correctedcolumn exists; no UI yet) - Exercise + set entry UI for workouts (routes exist; only workout creation has a form)
- Pagination on meals/form-session history
- Server Actions could replace some of these Route Handlers if you prefer that pattern