Four codebases, one product
FitScanned is the project where I own the whole vertical: the Flutter app people install, the NestJS API it talks to, the Next.js console the operators use, and the box all of it runs on. That is unusual enough to be worth explaining, because it changes how the decisions get made.
When the same person owns the client and the server, the contract between them stops being a negotiation and starts being a design choice. The error taxonomy below is the clearest example: it only exists because there was nobody to argue with about whose problem error handling was.
The scanning pipeline
A user photographs a meal. What happens next is four steps, and each one had a failure mode worth designing around.
Compression first. A modern phone camera produces a file far larger than the model needs. Sharp resizes and re-encodes server-side before anything else touches it, which cuts both the upload cost and the inference latency.
Object storage, keyed not copied. The image goes to S3 with intelligent tiering. The database stores the key; presigned URLs are generated on demand. Nothing in the API ever streams an image through itself.
Structured inference. Gemini is called with an explicit JSON schema across two model tiers: a fast, cheap default and a stronger model where the extra latency buys accuracy. The response deserialises into a typed DTO or it fails validation. There is no regex, and no "the model usually formats it correctly".
Typed all the way back. The result reaches the client as a DTO with an error code on the failure path, so the app can distinguish "we couldn't identify this" from "you're out of scans today" from "the service is down". Three situations that want three different pieces of UI.
Offline is a product requirement, not a nicety
People log meals in gym basements, on aeroplanes, and in kitchens with bad Wi-Fi. An app that throws away an action because the request failed is an app people stop trusting after the second time.
So the network layer sits behind a queue. The app records what the user meant to do and replays it when connectivity returns, with a connectivity watcher driving the retry. The user sees their meal logged. The sync is the app's problem, not theirs.
Running it on one box, on purpose
The original deployment was AWS EC2 with the usual constellation of managed services around it. The workload is one Postgres database and three Node processes. That is not a platform-scale problem, and it was not a platform-scale bill.
Now it is Docker Compose on a self-managed Hetzner host: Postgres, the API, the admin console and the landing site, with NGINX routing three subdomains in front. The cost fell sharply and the whole system became something a single engineer can hold in their head.
The honest counterweight: I now own the backups, the firewall rules, the TLS renewal and the OS upgrades. That is a real cost, paid in attention rather than in dollars. For a product at this stage it is the right side of the trade, and the reasoning is written into the repository so the decision can be revisited rather than inherited.
One piece of that deserves singling out. Postgres is never published to the host. Docker writes its own iptables rules and routes around the host firewall, so the obvious ports: ["5432:5432"] quietly exposes the database to the internet even on a machine with everything else locked down. The compose file binds it to the internal network and carries a comment explaining exactly that, because the next person who wants to connect a GUI client will otherwise "fix" it in about ninety seconds.
Beyond the app
Logging is not confined to the app. There are WhatsApp and Telegram bot surfaces, and an MCP server that lets an AI assistant act against the same API. A public scanner endpoint runs the pipeline without an account, using an optional-identity guard so the same controller serves signed-in and anonymous callers without a second code path.
Around the edges: RevenueCat for entitlements and subscription webhooks, PostHog for product analytics with an event taxonomy designed before the events were emitted, Sentry for errors, a structured audit log for the actions that matter legally, and FCM for the notifications that bring people back.