Slack app · Node · Firestore · Gemini
MedConnect: appointments from screenshots to reminders
Coordinating care across many clinicians means appointments arrive as portal screenshots, confirmation emails, and half-remembered phone calls. MedConnect lives in the household's Slack and turns each of those into a structured record, a calendar event, and reminders for the patient and a support person.
How a screenshot becomes an appointment
- Someone @mentions the bot with an image. The handler checks the sender against the owner and an allowlist of trusted uploaders set in the environment. Anyone else receives a polite refusal message, and the bot never downloads the image.
- One channel receives several kinds of screenshots (appointments, referrals, and lab orders), so the image is classified before it is routed: a cheap Gemini vision call returns a type, and each type's handler either claims the mention or lets it pass through.
- The claiming handler asks Gemini for a typed JSON shape (title, date, times, category, doctor hint, address). Prompts live next to the type they produce; unparseable output is an error, not a guess.
- The doctor hint is matched against the directory with a fuzzy matcher: substring or Levenshtein distance for whole names, then per-token matching that ignores titles such as "Dr.," which would otherwise match every doctor at once. No match means the bot asks, with a one-click "add this doctor" flow, rather than booking with the wrong clinician.
-
A single
bookAppointmentservice creates the Google Calendar event, writes the Firestore record with the event ID, and formats the Slack card. The slash command and the mention path both call it, so booking can go wrong in only one place.
Scheduled jobs
-
40-minute reminders run every five minutes and pick up appointments in a
35-to-45-minute window. Each record stores
reminderSentAt, so the job is idempotent across restarts and never sends a duplicate message. - Next-day follow-up at 11:00 a.m. Eastern (America/New_York) asks how the previous day's visit went and opens the record for visit notes and next steps.
- Referral and lab-order reminders nudge until the referral is resolved, and support-person reminders go to the second person on an appointment.
Structure
- Entry points
-
Slash commands
/appointment,/doctor,/directory,/find; @mentions with text or an image; button and modal interactions. - Layout
-
commands/,interactions/,events/,jobs/,services/,db/,types/,utils/. Each feature exports aregister(app)function;index.tsis only wiring. - Data
-
Typed Firestore documents:
Appointment,DoctorCard,Referral,TestReferral. Optional fields are documented at the type, including which Slack ID is set by which UI control. - Integrations
-
Gemini over plain
fetch(no SDK: one small function per prompt shape, typed return). Google Calendar through a service account. A hand-written RFC 5545.icsgenerator with a VTIMEZONE block, so a support person without calendar access can still import the event. - Runtime
- Node in a Docker image on Railway, with restart on failure. Unhandled rejections and exceptions are logged with a per-module prefix, so production logs can be grepped by feature.
Decisions I'd defend
- Classify before parsing, because the channel is not a reliable signal of content.
- Never invent a doctor. Ambiguity becomes a question with a button, not a best guess.
- Idempotency lives on the record, not in process memory, so restarts are safe.
- Slow work is visible work: the bot replies "Reading that referral…" immediately, then posts the result, so nobody wonders whether the mention landed.