DEV Community

Heron Developer
Heron Developer

Posted on

NutriTrack β€” How I Built a Full-Stack Nutrition Tracker with React, Supabase & Real Food Data

πŸ‘€ Who am I?

I'm a self-taught developer from Brazil, still learning and building things that I actually want to use. I've been studying web development for a while and decided it was time to build something more than a to-do list.
NutriTrack started as a weekend side project with one goal: stop guessing what I was eating. I wanted to know exactly how many calories and how much protein was in my daily meals β€” without paying for a subscription app, without dealing with a bloated interface, and without manually searching for every single food item.
So I built it. From scratch. With the stack I was learning.

The Problem I Was Trying to Solve πŸ’­

If you've ever tried to track your nutrition seriously, you know the pain. Most popular apps like MyFitnessPal are either behind a paywall for the features that actually matter, or they have a database focused entirely on American/European products β€” leaving Brazilian foods either missing or wrong.
I wanted something that:
β€’ Works with real Brazilian supermarket products
β€’ Lets me add custom foods manually when the database doesn't have what I need
β€’ Shows my daily progress clearly β€” calories in vs. my goal, protein in vs. my goal
β€’ Lets me plan meals ahead of time, not just log them after eating
β€’ Doesn't require me to carry my phone into a grocery store just to scan a barcode

That's NutriTrack. A nutrition control system designed around how Brazilians actually eat and shop β€” but flexible enough to work anywhere in the world.

What is NutriTrack? πŸ“Š

NutriTrack is a web application where you build your personal food bank, log what you eat each day, set daily nutrition goals, and schedule your meals for the next two weeks. It's connected to a real food product database so you don't have to type nutritional values by hand for every item.
Think of it as your personal nutrition control panel, not a social app, not a fitness tracker, not a recipe generator. Just clean data about what you're putting in your body β€” and how it compares to what you set as your target.
The core idea: if you can see exactly what you're eating and compare it against a goal you set yourself, you naturally start making better choices. No coach needed.

Features β€” and why each one matters

Social Login (Google & GitHub)

The first decision was: no passwords. Nobody wants to create yet another account with yet another password to remember. With Supabase Auth, I plugged in Google and GitHub OAuth in about an hour. πŸ”
Every user gets a unique account. All your food data, your daily logs, your goals β€” completely isolated from everyone else. If someone else signs up with a different email, they see absolutely nothing of yours. This is enforced at the database level using Row Level Security (RLS) in PostgreSQL, not just in the frontend code.
One interesting behavior worth knowing: if you log in with both Google and GitHub using the same email address, Supabase treats them as the same user β€” which is actually correct. You're the same person. Your data follows you regardless of how you log in.

Personal Food Bank

This is the foundation of the whole app. Before you can log anything, you build your personal food bank β€” a list of all the foods you regularly eat. Think of it like your pantry catalog. πŸ₯—
For each food item you can store:
β€’ Name and brand
β€’ Calories, protein, carbohydrates and fat per serving
β€’ Serving size and unit (grams, ml, units, tablespoons β€” whatever makes sense)
β€’ A product photo (uploaded directly to Supabase Storage, or imported automatically)

The key feature here is the Open Food Facts integration. Instead of manually typing the nutritional table from the back of a package, you search the product name and the app hits the Open Food Facts API β€” a crowdsourced database with over 3 million products worldwide, including a strong Brazilian catalog.
Search for 'FeijΓ£o Kicaldo', 'Aveia Quaker', 'Whey Gold Standard', or any product you find at PΓ£o de AΓ§ΓΊcar, Carrefour or AtacadΓ£o β€” and it comes back with the full nutritional info and even the product photo. One click and it's in your food bank.
For foods that aren't in the database (homemade meals, restaurant dishes, that specific aΓ§aΓ­ bowl you get every Sunday), you can add them manually with your own estimated values.

Daily Plan β€” Your Nutritional Diary

Every day you log what you actually ate. You pick a food from your food bank, select the meal type, enter the quantity in grams (or whatever unit you set), and the app calculates everything instantly. πŸ“…
Meals are organized into categories:
β€’ CafΓ© da ManhΓ£ (Breakfast)
β€’ AlmoΓ§o (Lunch)
β€’ Lanche (Snack)
β€’ Jantar (Dinner)
β€’ PrΓ©-treino (Pre-workout)
β€’ PΓ³s-treino (Post-workout)

At the top of the page you see your daily totals in real-time: total calories consumed, total protein consumed, and a progress bar showing how far you are from your personal goal. If your goal is 2,500 kcal and you've eaten 1,800, the bar shows you're at 72%.
One technical decision I'm proud of: when you log a meal, the app saves a nutritional snapshot at that moment. This means if you later edit the food item (say, you got a more accurate calorie count for that food), your past logs are not affected. Your history is always accurate to what you actually tracked at that time.

14-Day Scheduling

Beyond logging what you already ate, NutriTrack lets you plan ahead. The scheduling page shows a 14-day grid where you can build complete meal plans for future days. πŸ“†
Each day card shows the planned total calories and protein at a glance. Open a day and you see every meal broken down: what's in breakfast, what's in lunch, how many calories each section adds up to.
The killer feature: the "Apply to Today" button. Built your perfect Monday meal plan? Hit that button and all those meals are instantly added to today's daily log. No re-entering anything. Your plan becomes your diary.
This is especially useful if you eat similar things on a routine β€” athletes, people doing a cut, anyone with a structured nutrition protocol.

Personalized Daily Goals

Not everyone has the same needs. A 70kg person trying to maintain weight has different calorie targets than a 90kg person trying to build muscle. NutriTrack doesn't impose default values β€” you set your own targets. 🎯
You define your daily goals for:
β€’ Total calories (kcal)
β€’ Total protein (grams)

The daily plan page then shows your progress as a percentage of those targets. It's simple but it changes how you interact with food β€” you stop thinking in absolute numbers and start thinking proportionally. 'I'm at 60% of calories but only 35% of protein β€” I need a higher-protein dinner.'

Nearby Stores Map

A bonus feature that actually comes in handy: find supermarkets and pharmacies near your current location. This uses GPS from your browser, the OpenStreetMap database, and the Overpass API β€” completely free, no API key needed. πŸ—ΊοΈ
You can filter by store type (supermarket vs. pharmacy), radius (from 500m up to 60km), country, and city. The country and city filters are smart: they detect location by GPS coordinates, not by street name β€” which matters a lot in border regions. The app correctly separates cities if they border other countries ones, even though they're all within a few kilometers of each other.
The city filter also normalizes variations: 'City Two', 'City two', 'city Two' all collapse into a single filter option showing the most common written form.

Tech Stack β€” and why I chose each piece

Technology / Role / Why this over alternatives πŸ› οΈ
React 18 + Vite Frontend SPA / Instant HMR, zero config, I know the ecosystem
Tailwind CSS / Styling CSS variables for theming, no class conflicts
Supabase / Backend + Auth + DB + Storage PostgreSQL + OAuth + file storage in one free tier
React Router v6 Client-side routing / Declarative routes, no page reloads
Open Food Facts API / Food product database / Free, 3M+ products, strong Brazilian catalog
OpenStreetMap / Overpass / Nearby stores map / Completely free, no API key, global coverage

The most important choice was Supabase. I initially considered Firebase, but the fact that Supabase uses real PostgreSQL was a dealbreaker in its favor. I wanted proper relational tables, real SQL queries, and Row Level Security β€” none of which Firebase offers in a clean way. The free tier is generous enough for a personal project, and the dashboard is one of the best developer experiences I've used.
For the frontend build tool, Vite over Create React App was a no-brainer in 2024. CRA is essentially deprecated. Vite's startup and HMR speeds make development feel instant.

How the Architecture Works

The db.js layer β€” single source of truth for database access πŸ—οΈ
Every single database operation goes through a central file called db.js. This file has one job: get the current user's ID from the active session and attach it to every query.
Why does this matter? Early in development I had a bug where data from one account would bleed into another account's view when switching sessions. The root cause was that some queries were built when the component mounted β€” capturing the old user ID in a closure β€” and not re-evaluated when the auth state changed.
The fix was to make every query call getUid() at execution time, not at component mount time. Now it's impossible to accidentally query another user's data because the user ID is always fetched fresh from the active session right before the database call.

Row Level Security β€” the real security layer

Even though the frontend always filters by user_id, the real security lives in the database. Every table in Supabase has RLS policies that look like this:
SELECT: only return rows where auth.uid() = user_id INSERT: only allow if auth.uid() = user_id UPDATE: only allow if auth.uid() = user_id DELETE: only allow if auth.uid() = user_id
This means even if someone bypasses the frontend completely and hits the Supabase API directly with a crafted request, they cannot read, write, or delete another user's data. The database itself rejects it.

Nutritional snapshots β€” immutable history

When you log '150g of chicken breast' on a Tuesday, the app saves the nutritional values as they were at that moment: snap_calories, snap_protein, snap_carbs, snap_fat. These are stored directly in the log row.
Later, if you realize the chicken breast entry had the wrong protein value and you update it, your Tuesday log is not affected. Your historical data is always accurate to what you actually tracked at that time. This is especially important for people tracking over months.

Things I Learned Building This

The PKCE vs Implicit flow OAuth bug πŸ’‘
When I first set up Google and GitHub login, I got mysterious 401 errors after the OAuth redirect. The issue: Supabase's default PKCE flow generates a code_verifier and stores it in localStorage before the redirect. But in localhost development, that storage gets wiped during the OAuth redirect, so when the app tries to exchange the code for a token, the verifier is gone.
The fix was switching to implicit flow in the Supabase client config. Not ideal for production at scale, but perfectly fine for this use case β€” and it made OAuth work reliably.

Account linking by email

Supabase (and most OAuth systems) link accounts by email. If your Google and GitHub accounts share the same email address, they're treated as the same person β€” same user_id, same data. This is the correct behavior from a UX perspective: you don't want to lose access to your data just because you chose a different login button.

OpenStreetMap data is collaborative β€” and messy

The nearby stores feature uses OpenStreetMap via the Overpass API. OSM data is crowdsourced, which means it's incredibly broad but not always accurate. A hotel that sells groceries might be tagged as shop=convenience. A car wash near food stalls might appear in results.
I built a filtering layer with a blacklist of OSM tags (tourism, leisure, historic, natural) and a keyword blacklist for store names (hotel, hostel, pet shop, bazar, etc.) to remove clearly wrong results. It's not perfect β€” OSM data will always have noise β€” but it's much cleaner than raw results.

City name normalization

The city filter for nearby stores receives raw strings from OpenStreetMap: 'City Two', 'city two', 'city Two', 'CITY TWO' β€” all referring to the same city. Without normalization, the filter showed the same city four or five times.
The solution: normalize everything to lowercase without accents as the grouping key, but display the most frequent raw variant as the label. So the user sees 'City Two once in the filter, but clicking it matches all stores regardless of how their city was typed in OSM.

Why this works specifically for Brazil πŸ‡§πŸ‡·

Brazilian nutrition apps have historically been either underfunded local products or translations of American apps that don't know what 'pΓ£o de queijo' or 'cuscuz nordestino' is.
NutriTrack hits the Open Food Facts API which has a surprisingly solid Brazilian product catalog, major supermarket brands like Kicaldo, Quaker BR, Olvebra, NestlΓ© BR, and hundreds of regional brands are all there with correct nutritional values in the Brazilian format (kcal per 100g, as required by ANVISA labeling).
For foods that aren't in the database, the manual entry is designed to be fast. You don't need to fill in 20 fields. Name, serving size, calories, protein β€” that's enough to get going. Carbs and fat are optional.
The nearby stores feature knows about the Brazilian territory, app interface is in Portuguese, all meal names, labels, buttons, and messages are localized for a Brazilian audience.

Database Structure (4 tables, all with RLS)

Table / What it stores Key design decision πŸ—„οΈ
food_items / Your personal food catalog / One row per food. All macros stored per serving. Photo URL optional.
daily_logs / What you ate each day / Nutritional snapshot columns / freeze values at log time. Logs never change when you edit the food.
scheduled_plans / Meal plans for future dates / References food_items by ID. When plan is applied, it creates daily_log rows with fresh snapshots.
user_goals / Your personal daily targets / One row per user. Upserted on save. Defaults to 2500 kcal / 150g protein if not set.

The Project is Open Source

NutriTrack is fully open source under the MIT license. If you want to run it yourself, everything is in the repo β€” including the full SQL schema to set up the Supabase database, a .env.example with the required variables, and step-by-step setup instructions in the README. πŸ”—
It runs locally with npm install && npm run dev. Deploy to Vercel takes about 5 minutes: connect the repo, add the two environment variables (Supabase URL and anon key), and it's live.
One important Vercel step most tutorials skip: after deploying, go back to Supabase β†’ Authentication β†’ URL Configuration and add your Vercel URL to the Redirect URLs list. Without this, OAuth login will fail in production.
If you find it useful, build something on top of it, or spot something that could be better β€” pull requests are welcome.

Developer

πŸ™ github.com/devheron/NutriTrack

🌐 https://nutri-track-seven.vercel.app/

Top comments (0)