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_events table;
  • returns 500 on a failed write so RevenueCat retries;
  • handles TRANSFER by 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 TRANSFER handling and changeUser/logIn/logOut care.
  • SDK packaging. purchases-js advertises 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 configure from 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

Share this project:

Updates

Submission history