-
-
Bentara Physiology Logo
-
Example of how the AI tutor can answer questions based on physiology
-
Example of the textbook style lessons that accompany the interactive models
-
Example of the questions that are used to reinforce learning and can be linked back to the interactive models to show the answer
-
Example of the textbook style lessons that accompany the interactive models
-
Example of an interactive graph that changes based on the user's inputs with the sliders in real time
-
Example of a real patient scenario that helps the user to link the physiology to real clinical practise and that links back to the simulator
-
Example of the sliders that allow for the values in the interactive model to be adjusted in real time by the user
-
The pricing page that users will see when signing up for the app
-
Example of an interactive anatomical diagram that changes based on the user's inputs with the sliders in real time
-
Where the user can change their account details for better user data collection
-
The different areas of pharmacology that the app currently covers
-
The physiological formulas that are important for exams can be calculated using this page
-
The home page for the app when the user has signed in
-
The page to add a code for institutional access or demo access
Inspiration
As a final year medical student, I understand the need for medical students and resident doctors to receive a high quality education to inform their clinical decision making. Each patient presents a different challenge, both in diagnosis and treatment, and having a solid foundation of knowledge to guide treatment for patients is imperative.
My own experience through medical school has left me searching for a better way to learn physiology. Physiology was taught to me with static diagrams, but it is a set of dynamic systems. Cardiac output is where two curves meet, not a number to memorise. I want medical students to interact with their teaching, and watch how the physiology responds in real time. As a visual learner, this helps me to understand the processes that keep our bodies functioning much better than simply reading from a textbook or website.
Bentara Physiology Lab is a collection of interactive simulators across multiple physiological themes, built for exams including the UKMLA, USMLE Step 1, MRCP Part 1, MRCS Part A and FRCA Primary. Each one runs a real quantitative model: Guyton's venous return and cardiac output curves, Suga-Sagawa ventricular elastance, Hodgkin-Huxley, and acid-base via Henderson-Hasselbalch:
$$\mathrm{pH} = 6.1 + \log_{10}\frac{[\mathrm{HCO_3^-}]}{0.03\cdot P_{\mathrm{CO_2}}}$$
and Winter's formula for expected respiratory compensation, \(P_{\mathrm{CO_2}} = 1.5\,[\mathrm{HCO_3^-}] + 8 \pm 2\).
A product this useful to students also has to be sustainable and financially viable, and that is where RevenueCat came in.
What it does
- Predict-then-run practice, and pattern discrimination with the controls hidden.
- A formula reference, a pharmacology hub and an AI tutor.
- Progress in localStorage, or synced to Postgres when signed in.
- Three simulators (cardiovascular, respiratory, glucose regulation) plus the formula reference and medications hub are free; everything else sits behind one entitlement,
full_access.
How I built it
One subscription, every platform. The web app (Vite, React, TypeScript, Supabase, Vercel) uses @revenuecat/purchases-js for Web Billing. The Expo companion uses react-native-purchases for App Store and Play. Both use the same offering, the same entitlement, and the Supabase user id as the RevenueCat App User ID. A subscriber who buys on the web is unlocked on their phone with no mapping table and no "restore" dance.
A webhook as the single source of truth. A Supabase edge function receives RevenueCat events and is the only writer of profiles.subscription_status; the client has no column grant on it. It:
- authenticates with a shared secret, compared in constant time;
- is idempotent on the event id, via a
billing_eventstable; - returns 500 on a failed write so RevenueCat retries;
- handles
TRANSFERby demoting the old account and promoting the new one.
Status derived from expiry, not event type. A CANCELLATION or BILLING_ISSUE does not end access. Only an expiry in the past, or EXPIRATION, does:
$$\text{status}=\begin{cases}\text{free} & t_{\text{expiry}} \le t_{\text{now}}\ \text{trialing} & t_{\text{expiry}} > t_{\text{now}},\ \text{period}=\text{TRIAL}\ \text{active} & \text{otherwise}\end{cases}$$
This makes the webhook correct even when events arrive out of order.
Live pricing, honest claims. Prices come from the current offering ($rc_monthly, $rc_annual); hard-coded fallbacks only show if RevenueCat is unreachable. The "Save £X a year" line is computed from live prices as \(12m - a\) (monthly £9.99, annual £99, so £20.88), rather than hand-typed. A typed "two months free" claim would have been misleading under UK consumer law.
Testable by design. The webhook logic is plain shared code imported by both the Deno function and a Vitest suite, so the tests exercise the deployed code. The repo has over 3,800 tests.
Institutions. Schools pay by purchase order against a Stripe invoice and get a licence; the entitlement hook resolves access as institution, subscription or none.
Challenges
- Webhook ordering and retries. Naively mapping event type to status breaks on cancel-then-renew and out-of-order delivery. Deriving status from the expiry timestamp fixed it.
- Account transfers. Anonymous-to-signed-in and account switches needed
TRANSFERhandling andchangeUser/logIn/logOutcare. - SDK packaging.
purchases-jsadvertises a stylesheet export that does not ship in 1.55, so the build failed until I stopped importing it. I also load the SDK dynamically so it only downloads on the pricing page. - Native setup races. A double
configurefrom concurrent callers, and an empty platform key that needed to fall through to the Test Store key. One shared in-flight setup promise fixed the first. - Store and legal compliance. Auto-renew disclosure next to the Subscribe button (App Store 3.1.2, Consumer Contracts Regulations), an always-reachable Restore, in-app account deletion (5.1.1(v)), and a privacy manifest. I also removed sending exam and training stage to RevenueCat as attributes, for data minimisation.
- Expo Go cannot load the native SDK, so testing purchases needs an EAS development build. RevenueCat's Test Store made this practical.
What I learned
- Let the server decide entitlement; let the client only ask.
- Use one stable identifier across platforms and most sync problems disappear.
- Derive state from facts (expiry) rather than from event streams.
- Compliance is product work, not an afterthought.
What's next
The app will continue to expand, focusing on increasing the scope of the physiology taught, as well as moving into the other core areas of medical education, such as: Microbiology, Radiology, Pathology, Biochemistry, Pharmacology & Anatomy.
Developing the UI further to increase the educational value is a key priority, and seeking collaboration with institutions and medical schools to enhance the capabilities and accuracy of the models is another goal.
Eventually, the goal is to take the app live on the App Store and Play Store, and launching a web app in tandem, so that users can access the software regardless of what type of device they are using.
Built With
- claude
- gemini
- opencode
- react-native
- revenuecat
- supabase
- typescript
- xcode
Log in or sign up for Devpost to join the conversation.