Inspiration

I trained for years and ate badly the entire time. Not from lack of effort — from lack of information. I never knew whether I was eating enough, and I never knew what to do once I walked into the gym.

The apps I tried made both problems worse. Logging a meal took two minutes I didn't have. The ones that were fast were toys: confetti, streak badges, cartoon mascots. I wanted the opposite — an instrument. Something that treats my data seriously and gets out of the way.

So I built it.

What it does

Vulkan is a nutrition and training tracker where nutrition is the main act and training is the support.

  • Log a meal in seconds. Point the camera at your plate and a vision model estimates the macros. Scan a barcode and it resolves against Open Food Facts. Or just say it out loud — voice dictation feeds a text model.
  • Know what to do in the gym. A routine generator builds your week from your experience level, available days, and equipment. Progression, supersets, and dropsets are handled for you.
  • Trust the number. Calorie targets adapt to what you actually did, not to what you claimed you'd do on signup.
  • Compete locally. A city leaderboard, friend duels, clubs of up to 100, and challenges from 2 to 8 people.

It works with no connection at all. Everything is local first; the cloud is a mirror, not a dependency.

How I built it

Mobile: React Native + Expo SDK 56, React 19, TypeScript in strict mode (zero any in the codebase), Expo Router with typed routes and the React Compiler on. Reanimated for motion. 33 feature domains, each owning its own state and its own cloud boundary.

Local database: SQLite via Drizzle ORM. Every read is reactive through a single shared subscription per table — the app renders from disk and never blocks on the network.

Backend: Supabase — Postgres, Auth, Storage, Row Level Security across 44 tables, 75 migrations, and 6 edge functions.

AI: OpenAI for vision (photo → macros), DeepSeek for text, both behind one edge function with a per-user quota, a global cost circuit breaker, and an explicit consent gate. No prompt leaves the device without permission.

The rest: RevenueCat for subscriptions, Sentry for crashes, i18next across 22 namespaces in English and Spanish, HealthKit and Health Connect for wearable data, and a hand-written native Android widget module.

Tests: 344 suites, 4,657 tests, plus 52 pgTAP suites running against real Postgres.

Challenges I ran into

The bug that wasn't where it looked. New tables kept failing to write with 42501 permission denied. It looked like a Row Level Security problem. It wasn't — a table created by a later migration doesn't inherit the project's original GRANT, and Postgres checks privileges before it ever evaluates your policy. Every new table now needs RLS and an explicit grant. The database test suite caught it on the first run.

The poison row. A validation trigger that rejects a bad row on a synced table is a trap. The rejection doesn't reach the user — it kills the sync push, the watermark never advances, and that account silently stops syncing that table forever. The fix was a change in philosophy: sanitize, never reject.

Calories that disagreed with themselves. Two code paths wrote burned calories to the same column using different formulas, 4.8% apart. I collapsed both onto the net-energy standard:

$$\text{kcal}_{\text{net}} = \frac{(\text{MET} - 1) \times 3.5 \times \text{kg} \times \text{minutes}}{200}$$

Subtracting the 1 MET you'd burn sitting still is what Apple and Garmin do, and it dropped every displayed number by roughly 20%. The hardest part wasn't the math — it was accepting that a number getting smaller was the bug being fixed.

CI that died after passing. The test suite printed a green summary and then the runner killed it. The runner had 2 vCPUs, Jest computed cpus - 1 = 1 worker and ran everything in-band, so the React Native and i18n runtimes piled into one heap until it burst. Capping workers and memory fixed months of red builds that everyone had blamed on flaky tests.

Accomplishments that I'm proud of

Photo-to-macros. You point a camera at a plate and the friction that made me quit every other tracker disappears.

Underneath it, the thing I'm prouder of is less visible: the app is genuinely offline-first. Not "degrades gracefully" — fully usable in a basement gym with no signal, syncing later without ever losing a rep.

What I learned

I started with no prior app development experience. The lesson that cost the most: the compiler is not a safety net. Type checks, linting, and 4,657 tests were all green while a routine navigated to days[-1], and while a calorie target changed depending on whether you minimized the app. Both were legal values in every place that produced them. Now, when I add a parameter to a calculation, I grep every caller and decide for each one — because no automated gate will ever tell me I forgot.

What's next for Vulkan

Shipping. The app is functionally complete and in pre-launch polish: store review, device QA, and the last production deploys.

After that: live rest timers on the lock screen (Live Activities on iOS, a proper native chronometer on Android), and an annual recap built like a geological core sample — one stratum per week, colored by how hot you ran.

The goal stays the same as day one: make tracking cost so little effort that staying consistent stops being the hard part.

Built With

Share this project:

Updates

Submission history