DoseRx
A bedside dose calculator for 127 drugs that rounds each dose to tablets and syrups Indonesian pharmacies sell

- drugs, each with dose range and caps
- 127
- clinical groups organizing the drug catalog
- 9
- smallest tablet fraction the app suggests
- ¼
On this page
The problem
A doctor works out a weight-based dose for every patient. Multiply the dose per kilogram by the weight, divide by the number of doses, convert to millilitres, and check against the maximum. For children, the dose depends on weight and a mistake can do harm. The usual tools are paper references, a phone calculator or a spreadsheet. None of them is quick to use with one hand at the bedside.
DoseRx is built for one practising doctor who works on a phone at the bedside and on a desktop. It needs no accounts and no server.
What I built
The maths lives in one place, apart from the screens
I kept the dose calculation separate from the interface. It takes typed inputs, checks them, applies the dose caps and returns either a result or an error. A screen has to check which one it got before it shows a number. So a failed calculation can never appear as a dose, and a broken value never reaches the doctor. The same engine serves every calculator and is tested on its own.
Doses snap to what an Indonesian pharmacy sells
After calculating a dose, the app suggests the tablet fraction or syrup volume to give, using preparations sold in Indonesian pharmacies. Tablets snap to fractions a person can cut: ¼, ½, ¾, 1, 1½ and 2. The app shows the dose actually delivered next to the target, so the doctor sees any rounding. The Puyer tab does the same for compounded powders. It calculates each drug in a batch and prints a combined recipe card with tablet counts and signa.
Every drug can carry more than one dose
The catalog first modelled one dose per drug. That broke once diazepam needed one dose by IV for a seizure and a different dose by mouth for anxiety, and once adult dosing sat next to a drug's paediatric numbers. I split "drug" from "dose": each drug now holds one or more regimens, one per route, indication and population, while every existing dose stayed exactly as it was, so past calculations still compute from the same numbers. The catalog has grown alongside this, from the original 8 seed drugs to 127, now organised into 9 clinical groups instead of the earlier 17 flat categories.
Unverified drugs wait behind a review gate
New drugs, routes and doses arrive faster than I can personally re-verify each one against a source. I built a separate review state instead. A new entry starts as a draft with its source cited and stays invisible outside review mode, marked as unverified everywhere it would otherwise show. Only I can move it into the reviewed catalog. As of the most recent commit, the draft list is empty, meaning the current drug list has been through that check.
Navigation follows the clinical task at hand
The app began with four tabs: Preset, Custom, Puyer, Infus. That matched the four calculation engines, not how a doctor actually thinks at the bedside, closer to what an emergency needs than which calculator to open. I replaced the tabs with task-based destinations instead: Obat, Darurat, Puyer, Cairan and Riwayat, with one shared patient-weight bar carried across all of them.
Result
DoseRx is live on GitHub Pages and installs to a phone's home screen, working offline once loaded. Recent and starred drugs sit at the top of the picker, keeping the 127-drug catalog fast to search with one hand, and every calculation is saved in the browser with an optional note. The original spec had 8 drugs and three panels, built over about three and a half weeks in June and July 2026. I kept returning to it through September for a second pass that replaced the tab structure with task-based navigation, added the review workflow for unverified drugs, and grew the catalog to its current size. The app states plainly that it is a calculation aid, not clinical decision support.
Under the hoodTechnical detail for engineers
- React 18 and TypeScript on Vite, with
vite-plugin-pwa. Plain CSS. No UI, state or router library; routing is a small custom router rather than react-router. src/lib/calculate.tshas no DOM or React code and returnsCalcResult | CalcError. The UI branches onresult.validbefore reading any number, which removes the NaN and Infinity display bugs by construction. The same pattern coverscalculateInfusion.ts,calculateElectrolyte.ts,calculateFluidRate.tsandcalculateDextrose.ts.calculate.test.tsin Vitest covers normal doses, daily and single caps, missing concentration and invalid input.calculateInfusion.tssupports mcg/kg/min, mcg/kg/hr, mg/kg/hr and unit/kg/hr, and outputs a pump rate in mL/hr plus macro and micro drip rates.estimateWeight.tsimplements the APLS and Luscombe-Owens paediatric weight-from-age formulas, labelled as an estimate wherever it's shown.suggest.tssnaps doses to aSOLID_FRACTIONSarray.catalog.tslayersCatalogDrug/Regimenon top of the existing preset and infusion data without rewriting it, so every regimen still holds the original object the tests were written against. The catalog spans 164 dosing regimens across its 127 drugs.- New drugs enter
drafts.tswithstatus: 'draft'and a cited source; a clinician republishes one into a domain file withstatus: 'verified'and a reviewer name to make it live — a code change, not a data toggle. - The catalog lives in seven domain modules (analgesics, anti-infectives, antiparasitics, cardiorespiratory, emergency, gastro-allergy, steroids-nutrition) plus
requested.ts(the ward-requested list) anddrafts.ts, behind a barrelindex.ts. TheDrugPresettype separates calculation fields from display-only metadata. - Page metadata (title, description, OG/Twitter tags, the PWA manifest, sitemap) all generate from one
site.ts/routes.tsdata source at build time, after the manifest and page description had drifted out of sync once. - The build runs a typecheck, a client Vite build, an SSR Vite build of the entry point, then a Node prerender script that expands every route, including one per drug, into static HTML for GitHub Pages.
- A per-dose / per-day entry toggle is saved and applied across the app. When a preset dose is edited within its valid range, an amber marker and reset button appear.
- History goes through
storage.ts, a typed wrapper and the only code that toucheslocalStoragedirectly. Custom presets, recents and favourites are also saved locally, keyed by a stable drug id. - A dark-mode and accessibility pass added a medical-teal palette, documented as "Ward Day / Ward Night" design tokens.
- 165 tests across 20 files cover the calculation engines, catalog integrity, search, route matching, pack-dose suggestion, dose-position banding, style tokens, and a smoke test that renders every drug page.
- GitHub Actions deploys to GitHub Pages. About 15,400 lines of TypeScript, 134 commits between 20 June and 27 September 2026.
Screenshots
Hiring, or building something similar?
I'm open to full-time roles and select freelance work.