← All projects

Software · Health

NoBullFit

A privacy-first health and fitness tracker. I build the whole thing, the Rust backend, the React frontend, the mobile apps, and the unglamorous infrastructure in between.

Year
2025 →
Role
Founder · End-to-end
Status
Live

What it does

NoBullFit is a daily health log. You track food by meal, scan a barcode instead of typing thirteen digits, write and share recipes, record activities and strength sets, log your weight and your water, and keep grocery lists. It reads back as a dashboard of charts, a streak, a weekly review, and a PDF report you can hand to a trainer or a doctor.

A new account gets a Guide that turns the blank slate into a short path. Set a goal. Log a first day. Step on the scale. One step at a time.

There is a free tier and a Pro tier through Paddle. Pro is about planning ahead: meals and workouts laid out on future dates, copying a day or a week, weight goals with macro recommendations and a projected date, weekly insights, and control over what a report contains.

Architecture

The backend is Rust. axum for the HTTP layer, sqlx over PostgreSQL, and the migrations embedded in the binary and applied at startup, so a fresh checkout only needs a running Postgres. The frontend is React 19 with Vite, TypeScript, and Tailwind, talking to the backend over a plain JSON API.

The numbers people read every day are computed in one place. Weight trend, measured burn rate, goal timeline, and macro targets all come out of a pure function with no database access, so its behaviour is pinned down by unit tests instead of by whatever a query returned that day. Each of those stays empty until it has enough real data behind it, and the app reports what is still missing instead of showing a confident guess.

Every backend test builds its own throwaway Postgres database, migrates it, and drops it at the end. The suite runs in parallel against real SQL, so the tests exercise the constraints and the cascades rather than an imitation of them.

The food catalog

Food search runs on our own copy of Open Food Facts, a few million products imported into Postgres and queried there. No third-party nutrition API is called on a keystroke, which is faster and also means nobody outside the server learns what a member is eating.

Barcodes are decoded on the device: the browser's native detector where it exists, a WebAssembly reader in Safari and Firefox, the OS scanner inside the apps. Only the digits reach the server. When a barcode is missing from the catalog, the person holding the package can add it once, and every later scan of that product finds it.

Every logged entry keeps its own snapshot of the macros. Refreshing the catalog changes future lookups and never rewrites what a past diary day said.

One codebase, three apps

The iOS and Android apps are Capacitor shells around the same frontend bundle the website serves. There is no second mobile codebase. The handful of native differences are runtime checks on the same build: the status bar, downloads that go through the system share sheet, the OS barcode scanner, and, on iOS, no way to buy a subscription, because App Store rules say so.

iOS also gets Apple Health. The app reads body weight and steps and writes back the nutrition that was logged, opt-in and free, and nothing else is read or shared. Workouts were left out on purpose: HealthKit's active energy already contains them, so importing it would count twice against a calorie budget people read every morning.

Auditability

Every request is logged with a request id, method, endpoint, status, and duration, to the console and to rolling JSON files. Security-relevant actions get their own audit event on top of that: sign-ins and failed attempts, password and email changes, exports, deletions, billing webhooks, admin actions.

The request id comes back in the response headers, so an error a user reports leads to the exact lines that produced it. Passwords, tokens, and session identifiers are never written, and the client address is resolved by the server before logging, so a spoofed header cannot poison the log or the rate limiter.

Privacy by design

Passwords are hashed with argon2id. Sessions live on the server and only the SHA-256 hash of the token is stored, so the database cannot hand back a working session. Signup, login, and password reset sit behind Cloudflare Turnstile rather than a captcha that has to profile the visitor to work.

The export is real. Password confirmed, rate limited, and a full JSON dump of every user-scoped table. Completeness is enforced by the test suite: adding a column to a user table fails the build until it is classified as exported or excluded with a written reason. Deletion is granular, scoped to a window or total, and cascades through the schema.

It is honest to call this privacy-conscious rather than maximal. There are no client-side trackers, though the CDN in front of the site counts requests, and the logs still carry an email and an IP so the platform can be operated. The point is that each privacy claim maps to a mechanism in the code rather than to a paragraph in a policy.

Architecture highlights
  • Self-hosted food catalog. Open Food Facts imported into Postgres and queried there. No third-party nutrition API sees a search.
  • Barcodes decoded on the device. Native detector, WebAssembly reader, or the OS scanner in the apps. Only the digits leave the phone.
  • Insights with no database. Trend, measured burn rate, projected date, and macro targets come out of a pure function pinned by unit tests.
  • Real test databases. Every backend test creates, migrates, and drops its own Postgres database, in parallel.
  • Request-id audit trail. Every request and every security event written to rolling JSON, never the secrets.
  • An export that cannot fall behind. A policy list plus tests that fail when a new user column is neither exported nor excluded.
  • Two keys for the back office. A durable admin flag plus a short-lived elevation that ends when the operator leaves the site.
Stack
Rust · axum · React · TypeScript · PostgreSQL