BariCare
Daily check-ins after surgery, with alerts that reach the surgeon.
- Kind
- Client project. Deployed, pre-pilot.
- Timeline
- July 2026 to now
- Team
- Solo, working directly with the surgeon's unit.
- Built with
- Next.js, React, TypeScript, Drizzle, PostgreSQL, WebAuthn, Web Push, Tailwind CSS, Bun
01Context
After bariatric surgery most patients recover at home, and the early signs of a complication are things the patient notices first: pain that changes, a fever, a fast pulse.
The unit this was built for has no nurses on this pathway. The doctors who operated are the only people who can act on a warning sign. So the product is small on purpose. A patient answers 15 questions a day for seven days, and an answer in the red or emergency range reaches the surgeons' phones.
I treated it as a system where a bug is a missed clinical event, not a bad review. That sentence decided most of what follows.
02My role
Everything: scope, data model, every screen, the alert path, deployment.
Sign-in is for the unit's patients and doctors, and the repository is private.
03An alert that arrives exactly once, even when things fail
The hard problems here are not screens. They are the ways an alert can quietly fail to exist. A patient locks the phone halfway through the form. The operating system drops a push notification. Two servers send the same reminder. A scheduled job stops running, and nothing on any screen changes.
Try it: answers in, severity out
A reconstruction with a made-up protocol of four questions. It is not medical guidance and not what the unit uses. The five-minute window runs thirty times faster here.
What the patient sees
What the system derives
- Severity
- Green: nothing to act on
- Red flags
- 0
- Answered
- 0 of 4
What it does
- No notification
- Escalation timer not armed
- Check-in continues
04Decisions
Where does severity come from?
- Options
- Add up red flags in memory as the patient answers
- Store a running severity on the check-in
- Work it out again from the stored answers every time
- Chose
- The alert engine is a pure function from a protocol and an answer to a severity. After every write, severity and the red-flag count are derived again from all stored answers.
- Because
- A patient who locks the phone mid-form comes back to the first unanswered question with nothing lost and nothing counted twice. And because the protocol is passed in instead of imported, every red and emergency option gets a generated test.
- It costs
- Recomputing is wasteful in principle. With 15 answers it costs nothing that matters.
What happens when nobody acknowledges a red alert?
- Options
- Send the push again
- Escalate on a different channel
- Keep alerting until someone responds
- Chose
- A red or emergency answer sends a push to every doctor and schedules a job five minutes out. If nobody has acknowledged by then, the job sends an SMS. Once.
- Because
- The first alert was a push. If it went unacknowledged, the likeliest reason is that it never arrived, so sending another push is retrying the thing that just failed.
- It costs
- Five minutes is aggressive. If acknowledging turns out to be unreliable in practice, every alert will escalate and the SMS will stop meaning anything. The window is one settings row, and it is the first thing to watch in a pilot.
How do you make "notify each doctor once" true?
- Options
- Check before sending, at every place that sends
- Let the database refuse duplicates
- Chose
- A partial unique index. One red submission can produce one notification per doctor, however many red answers it contains and however many code paths try.
- Because
- A rule that depends on every caller remembering it will be forgotten by the next caller.
- It costs
- The rule lives in a migration, where it is less visible than code. It is written down as a numbered invariant, with a test named after it.
How do you notice a scheduler that has stopped?
- Options
- Trust the platform's cron dashboard
- Make the dispatcher prove it did work
- Chose
- Every reminder and escalation goes through one jobs table, drained once a minute. The dispatcher writes a heartbeat to the audit log and raises an alarm if no run has succeeded in the last hour.
- Because
- A cron endpoint can answer 200 and do nothing. Here it nearly did: the language redirect would have swallowed every cron request, because Vercel Cron does not follow redirects.
- It costs
- It is one more thing that can interrupt a doctor for a reason that is not clinical.
How do people sign in without a password?
- Options
- Email and password
- SMS codes only
- SMS, a PIN, and passkeys for doctors
- Chose
- Personal ID, an SMS code, a PIN, then optional biometrics. Doctors must register a passkey, and the passkey can replace the SMS step on later sign-ins.
- Because
- If the SMS provider has an outage, the doctors can still get in and see alerts.
- It costs
- A passwordless flow has more states than a password form. By commit count it is the most hardened part of the codebase.
Going deeper
History is frozen, not versioned
The questionnaire lives in the database and is edited in place. There are no versions and no publish step. That is safe because each answer stores its own snapshot of the question's wording and the label that was chosen. Rewording or deleting a question cannot change what a patient was asked.
Saving a change to the protocol asks for the PIN again, even inside an unlocked session. It is the one action whose mistakes are both silent and clinical.
What a patient is never shown
Nothing in the patient-facing types carries a severity. There is no colour, weight or ordering that could tell a patient which of their answers looks alarming, because on that side of the app the field does not exist.
Claiming jobs
Jobs are claimed with FOR UPDATE SKIP LOCKED, so two overlapping runs take different rows instead of both sending the same reminder. A job that fails retries three times with backoff, then appears as failed on the doctor dashboard. Locks older than five minutes are reclaimed, so a crashed run strands nothing.
SELECT id FROM scheduled_jobs
WHERE status = 'PENDING' AND run_at <= now()
FOR UPDATE SKIP LOCKED;An export that forgets dates
The research export removes names and IDs, and then goes further. No dates leave the system, only day offsets from the operation. In a unit that performs a known number of operations, an operation date plus a submission time is close to a fingerprint.
05The result
What works
- Deployed at baricare.ge. Every screen in the specification is built: the patient check-in and history, the doctor queue with a live escalation countdown, patient registration, the protocol editor, the team screen and an anonymised research export.
- 37 test files, including one generated test for every red and emergency option.
- Georgian first, with English available.
What is not there
- Pre-pilot. It has not been clinically validated, and nothing here claims that it has.
- A known bug, documented and not yet fixed: if a patient starts a second check-in after an emergency stop, the doctor queue's one-row summary shows the newer check-in instead of the unacknowledged emergency. The patient page is correct.
- The test suite talks to a shared remote database, so a failing test can leave rows behind.
06Reflection
Two bugs reached the client on a real device, because I trusted a typecheck over a picture and a platform string over the documentation. Both are written up in the notes. The rule I took from them is small: look at the rendered thing, and look up the API.
The cost I would weigh earliest next time is test isolation. Tests against a real Postgres caught things a mock never would, since the invariants live in the schema. But every month on a shared branch makes a schema per test more expensive to adopt.