pharmacy supplement platform
took over a live health-supplement app with half its source missing, made it buildable, then rebuilt around it
- when
- Sept 2026 – now
- for
- a pharmacy health-supplement recommendation platform
- role
- solo: takeover, build, deploy, operate
- stack
- flutter, next.js, react, spring kotlin, python, postgresql, github actions, docker
- size
- 139 commits, 8 codebases, ~33k lines written on top of a 1,445-file inherited codebase
- client work, through neoworks
also in 한국어
what it is
this is a korean app that looks at your national health checkup and prescription data, recommends supplement ingredients, and lets you order them from a nearby pharmacy. there's a flutter app on both stores, a user web app, a partner site for pharmacies, an admin site, a spring backend and a python ai server. another agency built it. i took it over in september while it was already in production.
the hard part: inheriting something you can't rebuild
at first the handover was a zip of the flutter app and a pdf. the web frontends, the backend and the ai server were "to be shared separately", and the shared folder later started asking for a login.
the production server had build output only. the frontend had 51 source maps, none with sourcesContent. the partner and admin sites were static files behind nginx. i found a partner source zip on a laptop, but it was from august 7 and production was built on august 26. production had bulk registration and an excel template that this source didn't. deploying it would have quietly removed features pharmacies were using, so it stayed a do-not-deploy reference.
the full source arrived on september 29: 1,445 files. until then i worked on the parts i actually had.
making the app reproducible
the goal: push a v* tag, and ci builds, signs, uploads and submits both apps for store review. getting there was a list of small traps.
main.dartimportedwebview_screen.dart, but the file waswebView_screen.dart. fine on mac and windows. on a linux runner it's a missing file and the build dies.- gradle evaluated the signing config even with no keystore, so every build without
key.propertiescrashed on a null cast. now release signing only happens when the key exists, and the release workflow fails if it catches a debug-signed bundle. - the handed-over
.jkskeystore was actually pkcs12 inside. - the account's artifact storage was full,
upload-artifactfailed, and every step after it, including the play upload, got skipped. archiving a copy should never block a release, so that step can fail now. - build numbers are
100 + run number, so nobody bumps them by hand and the stores never see one go backwards.
the android release is signed in ci. ios signing and the final store submission still wait on credentials only the account owner can provide.
the emulator screenshot job kept freezing on github runners, so i replaced it with widget tests that render the app's screens straight to phone-sized pngs.
the offline screen, rendered by a widget test instead of an emulator
the second update
once the source landed, the work was about making it usable: one consent screen instead of several, coming back to the page you were on after login or payment, one start button on home, a questionnaire with progress and resume, and a results page that leads with the conclusion and gives one reason per ingredient.
the client called pharmacy product registration the most important part. it became search, pick, type the price per tablet. the monthly price is computed. later they could request products that aren't in the catalog, and once approved, other pharmacies can register them too.
the catalog had a mess underneath: the same ingredient stored as 비타민C, 비타민c, (10)비타민C. i normalized names on write and merged existing duplicates in one reviewed sql run: 406 groups, 651 ingredients deactivated (nothing deleted), 44,246 product links repointed. the ai recommendation candidates were protected on purpose, because the rule from the start was not to change the recommendation engine, only what sits around it.
the ai chat had its own bug. every message was routed as a survey answer, so the user's actual question reached the model as an empty string, and it gave the same reply to everything.
for pharmacists i wrote a windows tray app that pulls print jobs from the server and prints a one-page consultation summary. it renders the receipt as an image with an embedded korean font, so the printer's code page doesn't matter.
an 80mm receipt from the printer app's ci build, with sample data
running it
there was no deploy pipeline left after the takeover. i wrote one: build locally, ship the image over ssh, swap containers, health check, and keep the previous image as a rollback tag. on september 30 all three tls certificates expired because the server had no renewal at all, and https was down for about 2h20m. there's a systemd timer for that now. on a later backend deploy the new container failed to start on a missing env var and rolled itself back after 3 to 4 minutes of downtime. since then the script starts the new image on a spare port first and only swaps when it's healthy.
where it is now
live and still moving. web, partner, admin and backend deploy from one repo with a shared ui package. pharmacy order notifications are deployed but switched off until their message templates are approved.