DEV Community

Ehsan Abedi
Ehsan Abedi

Posted on Originally published at ehsanabedi.com

Building a Clinic Booking App in Flutter: Slots, Reminders and Privacy

Clinic booking apps look like a calendar with a "Book" button. In practice, the hard parts are elsewhere: how you model free time, how you stop two patients from taking the same slot, how reminders reach people in the right time zone, and how you keep health data out of places it shouldn't be.

I'm a freelance Flutter and WordPress developer in Famagusta, North Cyprus, where many clinics treat local patients and visitors from abroad in the same week. Here is how I structure these apps.

Model availability, not appointments

Storing only appointments and "calculating" free time on the client gets messy fast. I keep three things on the server:

  • Schedules: each practitioner's working hours per weekday, per branch.
  • Exceptions: holidays, sick days, a surgery that blocks the afternoon.
  • Bookings: practitioner, branch, start, end, treatment, status.

Free slots are generated on the server for a requested day: working hours minus exceptions minus existing bookings, cut into pieces of the treatment's duration plus a buffer. The app only displays what the server returns.

Never trust the client with the last slot

Two patients tapping the same 10:30 slot at the same moment is not a rare edge case on a Monday morning. The booking must be created in a transaction (or with a unique constraint on practitioner + start time) on the server. With Firestore, that means a transaction that reads the slot document and fails if it already exists. With Supabase / Postgres, an exclusion constraint on a time range does the job elegantly:

ALTER TABLE bookings
  ADD CONSTRAINT no_overlap
  EXCLUDE USING gist (
    practitioner_id WITH =,
    tstzrange(starts_at, ends_at) WITH &&
  ) WHERE (status IN ('pending', 'confirmed'));
Enter fullscreen mode Exit fullscreen mode

The app then simply handles the error with "This time was just taken, here are the next free times".

Time zones: store UTC, show local

Patients book from abroad before they travel. Store starts_at in UTC, show it in the clinic's time zone with the zone name visible ("10:30, Cyprus time"), and schedule reminders from the server, not with local notifications calculated on the phone. A reminder the day before and a short one two hours before cuts no-shows noticeably.

Forms before the visit

Intake forms are where privacy matters most. My rules:

  • Collect only what the clinic actually needs.
  • Store answers server-side with strict access rules (staff role only), never in local storage or analytics events.
  • No health details in push notification text: "You have an appointment tomorrow at 10:30" is fine, the treatment name is not.

One codebase, two front doors

New patients usually find the clinic through Google and book on the website, while returning patients use the app. Both talk to the same booking API, so the calendar is always the single source of truth. Flutter covers Android and iOS from one codebase, and the website can be WordPress with a small booking front-end calling the same endpoints.


The business-side version, for clinic owners, is here: Clinic Appointment App in North Cyprus: Fewer No-Shows, Happier Patients. How do you handle slot collisions in your booking apps?

Top comments (1)

Collapse
 
stacklyn_labs profile image
Stacklyn Labs • • Edited

The exclusion constraint is the cleanest answer to your closing question, and it's the one we'd reach for too. A few gotchas worth adding for anyone copying it:

  1. It needs btree_gist. Using practitioner_id WITH = inside a GiST exclusion constraint requires CREATE EXTENSION btree_gist; first, otherwise Postgres rejects the constraint.
  2. Pending holds need an expiry. If pending blocks the slot, a patient who abandons the form holds 10:30 forever. The predicate can't use now() (it must be immutable), so a simple fix is a hold_expires_at column plus a small scheduled job that flips stale pending rows to expired, which drops them out of the constraint.
  3. Store working hours as local wall time, not UTC. Bookings in UTC is right, but a schedule like "09:00 to 17:00" should be stored with the clinic's IANA zone (Asia/Nicosia) and converted per date. Cyprus changes clocks twice a year, so hours stored as UTC drift by an hour at every DST switch.

Agree completely on keeping treatment names out of push text. Lock screens are more public than people think.

(We build Flutter apps at Stacklyn Labs, including a vet clinic app, so slot modelling is a familiar headache.)