Garvit Galgat

STARTED MAR 2026 · PAID SINCE APR 2026 · TECHNICAL CO-FOUNDER

PanOph

An ophthalmology learning platform for residents

Visit website

Next.js · TypeScript · Flutter · Hono · Cloudflare Workers · D1 · R2 · KV · Firebase Auth · Razorpay · RevenueCat

At a glance

Role
Technical co-founder and the only engineer
Speed
Idea to paid launch in 2 months
Traction
165 paying customers · ₹5.5L+ collected since Apr 2026 · 130 active subscribers
Decisions
Self-healing billing · an offline-safe outbox that cut writes by 80% · a staged, fail-closed AI content pipeline
Status
Built and in learners’ hands; it now needs only occasional input from me

PanOph is an ophthalmology learning platform for residents, co-founded with an ophthalmologist, Dr. Rahul Arya. The team also includes two clinical reviewers and two people on content and social media. As technical co-founder and its only engineer, I built the whole technical side, mostly on weekends alongside my role at Oracle: the Next.js web app, the Flutter mobile app, the Cloudflare Workers API, and separate review, admin, creator, and image-contribution apps.

Most learners find it through Instagram, where @pan.oph has a 9K+ audience run by a small content and social team.

Signed-in home on the PanOph web app, Oct 2026.
HOW IT WORKSFrom a reviewer’s knowledge to a student’s study screen.
  1. Reviewers create content

    Reviewers turn clinician-written PDFs into checked lessons. Contributors add teaching images.

    Review app + Image Vault · Next.js

  2. Content is published

    Reviewed content is released to the shared library, ready for students to study.

    Workers API · R2 content storage

  3. Students study

    Students read, practise, and return to their learning on web or mobile.

    Web app · Next.js / PWA · Flutter mobile app

    • Sign inOne identity across apps · Firebase Auth
    • Access a subscriptionWeb payments · Razorpay
      Mobile purchases · RevenueCat
    • Keep learning progressProgress and access records · D1 database

Where it started

The co-founder had lived the problem: the material residents study is scattered across textbooks and question banks, and they compile it by hand. The first version was a simulator for surgical decisions, letting residents practise the choices made in an operating theatre.

It found users through his college, and they kept asking for more: topic guides, drug references, and past exam papers. Each was added in turn. PanOph went paid at the end of April 2026, about two months after the first commit.

The scenarios that started it: 105 clinical cases across 10 specialties.
The Image Vault: watermarked teaching images contributed by clinicians.

From clinician PDFs to live lessons

The content pipeline went through four generations, and each one clarified where a model belongs and where it does not.

Prompting. The first version asked a model for JSON on a topic. It worked, but the lessons were shallow.

Validating. The second web-searched every claim. That was better, but slow and expensive, and it was still checking text a model had invented.

Clinicians as the source. The shift came from the co-founder. Working through long clinical conversations with an AI model, he wrote 359 topic PDFs across 11 specialties, far better than anything a cold prompt produced. PDFs were the best format for him to write in and the worst for the product: not editable, not searchable, and hard to gate or render on mobile. They became the source of truth, and the pipeline was rebuilt around them.

A staged, fail-closed pipeline. Converting a whole PDF in one request failed: long documents produced output that matched the schema but not the source, at a high cost per topic. Each stage now does one job:

  1. Deterministic extraction of text, table geometry, and page images, with no model involved.
  2. A classifier proposes reviewer-sized units, such as a table, a viva Q&A set, or a callout.
  3. A reviewer works page by page, with the PDF on one side and the proposed units on the other, confirming or fixing the structure.
  4. The manifest freezes only when every page is confirmed and every piece of source text has exactly one owner.
  5. A model then runs one small task per confirmed block, in parallel, and returns a builder descriptor, never JSON or prose.
  6. Deterministic builders assemble the topic with a provenance map back to the source. A fail-closed adapter refuses any conversion that would silently lose structure.

The caution comes from one benchmark run in which the model silently corrected a typo in the source. Harmless there; disqualifying for a dosage.

Medical validation. Faithful JSON is half the job. In the review app, a clinical reviewer selects the blocks they doubt, or everything, and an agent extracts each claim and validates it against sources, attaching evidence for the reviewer to accept or reject.

Those validation runs were the expensive part, so execution moved to a self-hosted runner. The reviewer’s browser talks to it directly over a Cloudflare Tunnel, and the runner owns the whole queue in SQLite: raw model output is saved before parsing, a single-owner lock and orphan fencing guard each job, and a shared cooldown applies when the provider rate-limits. Closing the browser or losing the tunnel does not cancel a job. The cloud side only authorises and saves.

Billing that heals itself

A user upgraded, paid, and their account did not change. The payment had succeeded; the webhook never arrived. Razorpay’s documentation says webhooks can be duplicated, are retried for up to 24 hours, and that the API should be queried when one has not arrived. An idempotency table already handled duplicates. The other two cases were not handled.

Added a “check payment” button on the account page that fetches the subscription and its payments directly from Razorpay, and a job every 30 minutes that finds any checkout or renewal that has gone quiet and does the same. The webhook, the button, and the job all feed one apply function, so whichever arrives first wins and the rest are no-ops.

A missed webhook is no longer an incident; it is a 30-minute delay.

The UPI double charge

The most serious bug came right after launch: some users were charged twice. Upgrades were a plan change on the existing subscription, which Razorpay supports for card mandates. For UPI it rejects the call, because a UPI AutoPay mandate is approved at the bank for one specific plan.

The workaround asked users to cancel and subscribe again. Cancellation deliberately took effect at the end of the billing cycle, so users kept the access they had paid for, which meant the old mandate stayed live and took one more payment while the new one charged immediately. The user record could also hold only one subscription ID, so the new subscription overwrote the old one and the system lost sight of it.

Refunded the affected users the same morning and audited every paying customer. Rebuilt plan changes so the new subscription is confirmed before the old one is cancelled. In July, moved subscriptions to one row per product, so a user can never again be double-subscribed without the system seeing it.

One approval at purchase

Users could buy a base plan and optionally an extension for past exam papers. The clean model was two subscriptions with independent billing and cancellation. In India, that means two Razorpay approval screens and two UPI mandates back to back. That drop-off looked costlier than the modelling compromise, so each base-plus-extension combination became its own plan with a single approval.

The cost was flexibility. Any later change to a bundle needs a fresh mandate, because a UPI mandate cannot be edited in place. The first data model also stored the bundle as one string, so the system could not reason about the two products separately; the July rebuild fixed that while keeping one mandate at purchase.

Plan changes turned out to be about 12% of checkouts, so the product decision held: buying, the common path, takes one step, and changing, the rare one, takes one more. The modelling would be different from day one: two rows under one subscription, so the bundle is a billing detail rather than the unit of data.

A pricing page rebuilt from one message

After launch, analytics showed people reaching the pricing page and leaving, with conversion well below expectations. The assumption was price, until a user wrote on Instagram that there were too many options to tell what they would actually get.

The page was a checklist: three plans with eight bullets each, a comparison table, and an FAQ. It told visitors everything and showed them nothing. Rebuilt it as a journey. Each plan became one line about who it is for and three highlights; the detail moved behind a “see what you get” preview; and each plan links to one real free sample, a scenario or a topic, with a sticky “back to pricing” button. Try it, come back, decide.

Progress sync without lost work

Progress began as one API call per action: mark a topic read, view a drug, save a favourite. A single reading session meant dozens of small D1 writes. On sign-in, the whole local progress record was also uploaded and overwrote whatever the server had. That was chatty, lost work on a closed tab or bad Wi-Fi, and let a phone that had not synced overwrite progress made on a laptop.

In July, replaced it with an outbox. Each action becomes an operation with its own ID, applied locally at once and queued on the device. A one-second debounce batches up to 15 operations into one request. The server applies the batch in a single database batch, records a receipt for each operation ID so a resent operation is acknowledged but never applied twice, and bumps a version number. The client drops acknowledged operations and pulls a fresh snapshot only when the version changes. Deletes are tombstones, so a stale device cannot undo them, and operations older than a week are not replayed; the device re-syncs instead of pushing stale intent.

Comparing the days before and after, D1 writes fell about 80%, from about 50,000 rows a day to about 10,000, from the outbox alone. A separate fix the same week moved the leaderboard from a rebuild every 30 minutes to a daily snapshot; that job had briefly pushed writes past 200,000 a day, and the change also roughly halved reads. The admin dashboard, which counted everything each time an admin signed in, now uses a 24-hour cache; approximate numbers are enough there.

Running it

Every route on Vercel logs its start, finish, and errors with a request ID and duration, and each Cloudflare request carries its Ray ID, so any call can be traced. Health checks and anomaly alerts have fired for real: one caught a 12× traffic spike after an Instagram post, served from cache at no extra cost; another caught an automated crawler hitting the site for ten minutes, handled through robots.txt.

The worst outage started in the review app. A React effect loop kept issuing database reads and pushed the Cloudflare account past its plan’s daily D1 read limit. Every app’s databases shared that account, so the main site stopped serving reads too. It happened at night and recovered when the daily limit reset. Upgraded the plan and added usage alerts for unusual spikes. The change to make next is structural: separate accounts for each production app, so one app’s limits cannot take down another.

The workload is read-heavy, and the platform runs for about $50 a month. There is no dedicated error tracker or paging yet.

Scale · October 2026

As of October 1, 2026: 165 paying customers since the paid launch, 130 active subscribers, and 67 renewals. ₹5.5 lakh+ collected after refunds from April to September 2026.

Separately, 2,000+ registered accounts, approximately 100 daily users, and a 9K+ Instagram audience at @pan.oph. The image library holds 499 published teaching images from 43 clinician contributors, from about 2 GB of uploads.