Inspiration
I was managing my own health with a spreadsheet and three apps that did not talk to each other.
Every nutrition tracker I tried had the same shape: it handed me a search box and asked me to become a data-entry clerk. Find the food. Pick the right one out of forty near-identical rows. Guess the portion. Do it again for the next item. I would keep it up for nine days and quit on the tenth — not because I stopped caring, but because the app had made caring expensive.
The second problem was worse and nobody seemed to be solving it. I was tracking what I ate in one place, my weight in another, my medications on paper, and my lab results in a folder of PDFs. Four datasets about one person, and no way to put them next to each other. The question I actually wanted answered — is what I am doing working? — was not a question any of those apps could take.
So Vyra started as the thing I wanted to exist: take a photo of the plate, get the numbers without hunting for them, and then see your food next to everything else you are already tracking.
What it does
Take a photo of a meal, or just say what you ate. Vyra counts your numbers and shows what is left in your day — no searching, no weighing, no math. It marks anything it had to estimate, so you always know which numbers are solid.
Then it connects what you eat with your weight, your movement and your medications, so you can see what is actually working.
It is a copilot, not an oracle. It shows you your own data organised well. It does not diagnose, it does not prescribe, and it does not tell you what your numbers mean about your health — that line is drawn deliberately and enforced in code, not just in policy.
How I built it
Solo, across a polyrepo, over several months.
Mobile — React Native and Expo with expo-router, TypeScript throughout. TanStack Query for server state, Zustand for UI state, Reanimated for motion, MMKV for the encrypted local store. Apple HealthKit and Android Health Connect for weight and activity.
Backend — Hono on AWS Lambda behind API Gateway, in TypeScript. Postgres with Drizzle and hand-written SQL migrations. Row-level security on every user-data table, scoped per request through a session GUC — a missing policy is treated as a bug, not a gap.
AI — Claude on Amazon Bedrock. Every model response is parsed against a zod schema from a shared contracts package before anything touches the database. The model maps what it sees to a food; a nutrition database supplies the actual numbers. That split is the whole reason the numbers are consistent instead of imaginative.
Offline-first everywhere. Every write hits the local encrypted store first and returns immediately; a sync engine pushes through an outbox with retries and hydrates on login, foreground and cold start. You can add a meal on a plane and it is simply there later. Conflicts resolve by last-write-wins per row with an append log and a local-to-server id map.
Infrastructure — AWS CDK, region-parameterised. Shared schemas ship as a versioned package consumed by both sides, so a contract change cannot silently break one half.
The monetization is the part I would show you first
I spent more design effort on the subscription than on any single feature, and RevenueCat is doing more here than "wrap a purchase."
The paywall is the last step of onboarding, not a wall behind it. The original build had the account come first. By the time a price appeared, the user had handed over an identity, accepted consent and answered a dozen questions — and then met a non-dismissible screen whose only exit was Sign out. Nothing in that sequence was individually wrong. The trap was the seam. The gate was never the bug; the order was.
So the paywall became an ordinary pushed route inside the funnel, which means Back works by construction — no navigator special-casing, nothing to keep in sync. Account creation is a sheet that the trial button opens.
The load-bearing rule: the commit is gated on the purchase, not the auth. Authentication and payment fail independently. Committing on auth would mark someone onboarded with no entitlement and route them straight into the hard gate — the original trap through a new door. Two exceptions are deliberate and tested: no RevenueCat key configured commits anyway (or a development build becomes uncompletable), and a resolved call with nothing to grant is not the same as a failure.
Enforcement is server-side; the client gate is UX. The expensive AI routes sit behind an entitlement check that returns a 402, which the mobile fetch interceptor turns into the paywall. The client can be wrong or out of date and the cost is still controlled. The two directions fail differently on purpose: app access fails open, so a network blip never locks a paying user out of their own data, while the AI write fails closed.
Three tiers, seven-day trial, and a subscription state machine where a lapsed user keeps full read access and cheap writes — you never lose your history, you lose the expensive parts.
Challenges
The purchase that succeeded and granted nothing. On the first iOS device test the Apple payment sheet appeared, the purchase completed, the app said "you're all set" — and then re-prompted the paywall. No error. Nothing in crash reporting, because from the app's point of view nothing had failed.
The cause is a genuinely non-obvious distinction: App Store Connect issues two different credentials that do two different jobs. The API key fetches product metadata, which is why prices and the paywall rendered perfectly. The In-App Purchase key is what lets RevenueCat validate the transaction server-side and actually grant the entitlement. I had configured the first and not the second, so Apple happily took the money and the entitlement never existed. Everything looked correct on every screen I thought to check.
The lockout that only happened to people who paid. During dogfooding the paywall became impassable after subscribing — you would buy, bounce back to the paywall, and Google would tell you that you were already subscribed. The gate trusted only my server, and my server flips to active when the store's webhook lands. In the seconds between a completed purchase and that webhook, the on-device SDK already held the entitlement and the server did not, so the gate bounced a user who had genuinely just paid.
The fix is the canonical pattern, learned the expensive way: trust the SDK for the gate, keep the server as the hard control for anything costly. A short-lived, per-user client grace flag set on a confirmed purchase opens navigation immediately, while server-side enforcement on the AI routes stays exactly as strict as it was.
Building a health product without becoming a medical device. This shaped more code than any other constraint. Store, structure, trend and show — never diagnose, prescribe or claim causation. It meant a lexeme prescan that diverts a message in distress to real crisis resources before the model is ever called and before any billing check, so a person in a bad moment is never met with a paywall. It meant a copy blocklist enforced in CI across every repo so no screen can shame you about food. It meant deciding that lab interpretation ships as structure and trend, never as a verdict.
What I learned
Store billing is not a feature you add at the end. Every hard bug I hit was a seam — between auth and payment, between the device and the webhook, between two Apple credentials that look interchangeable and are not. RevenueCat removes an enormous amount of platform work, and the thinking it does not remove is ordering: what is true on the device versus true on your server, and which one the user is standing in front of right now.
The other lesson is that constraints made the product better. Refusing to let the app tell people what their numbers mean forced me to make the numbers themselves clear enough that people can reach their own conclusions — which is what I wanted from a tracker in the first place.
What's next
A US public launch, then the connected layer I built the foundations for: putting food, weight, medications and lab results on one timeline so the "is this working?" question finally has somewhere to be asked.
Built With
- amazon-api-gateway
- amazon-bedrock
- amazon-web-services
- aws-cdk
- aws-lambda
- better-auth
- claude
- drizzle-orm
- expo-router
- expo.io
- health-connect
- healthkit
- hono
- mmkv
- postgresql
- posthog
- react-native
- react-native-reanimated
- revenuecat
- sentry
- tanstack-query
- typescript
- zod
- zustand

Log in or sign up for Devpost to join the conversation.