# Booking App for Clinics

> Patients book free slots with doctors, staff manage hours and see the day's schedule. Double booking is impossible.

- Category: Healthcare
- Version: 1.0
- Source: https://wonly.dhfm.in/builder-4392ac/clinic-booking-app/

## Overview

A real appointment system for a small clinic: patients sign up, pick a doctor and a free time, and manage their bookings. Staff set each doctor's weekly hours and see today's schedule.

What makes it solid: the database itself blocks double booking, and Row Level Security means patients only ever see their own appointments.

Before you start: Node.js 20+, a free Supabase project, and Claude Code or Cursor. About 2 hours.

## Tech stack

- Next.js
- shadcn/ui
- Supabase
- Tailwind

## Build steps

### 1. Create the project from the official starter

Create a free Supabase project first (supabase.com → New project). Then run in a terminal:

npx create-next-app -e with-supabase clinic-booking
cd clinic-booking

Rename .env.example to .env.local and paste your project URL and publishable (anon) key from Supabase → Project Settings → API. Run npm run dev.

Prompt:

```text
Read the project structure and README. Explain in 5 bullet points how auth works in this starter (where the Supabase clients live, how the session cookie is refreshed). Do not change any files yet.
```

**Done when:** You can sign up and sign in at localhost:3000.

### 2. Add the project rules and the Supabase MCP

Save CLAUDE.md in the project root. Create a personal access token in Supabase (Account → Access Tokens) and add the Supabase MCP with your project ref. Keep --read-only: your AI can read the schema but not change data.

Prompt:

```text
Read CLAUDE.md and confirm the rules in one short list. Then use the Supabase MCP to list the tables in my project.
```

**Done when:** The MCP is connected (in Claude Code, /mcp shows supabase) and lists your tables.

### 3. Create the database

Open Supabase → SQL Editor, paste supabase-schema.sql and run it. Sign up once in the app, then run the commented line at the bottom of the file with your email to make yourself staff.

Prompt:

```text
Save the schema I ran as supabase/schema.sql. Generate TypeScript types for it into lib/database.types.ts (run: npx supabase gen types typescript --project-id YOUR_PROJECT_REF > lib/database.types.ts) and use them in the Supabase clients.
```

**Done when:** Four tables exist with RLS on, and npm run build passes with the generated types.

### 4. Onboarding and roles

Every new user gets a profile automatically (database trigger). Ask for a name and phone number once, and route people by role.

Prompt:

```text
After sign-in, if profiles.full_name is empty, show /onboarding with name and phone fields (Server Action to save). Then send patients to /book and staff to /staff. Add a header with the user name, role and a sign-out button.
```

**Done when:** A new account is asked for name and phone once, then lands on the right page for its role.

### 5. Staff: doctors and weekly hours

Staff add doctors (name, specialty, slot length) and weekly hours (e.g. Mon 09:00–13:00 and 15:00–18:00).

Prompt:

```text
Build /staff/doctors: list, add, edit and deactivate doctors, and edit each doctor's weekly hours (rows of weekday + start + end, add/remove rows). Only staff may open /staff pages: check the role on the server and redirect others to /book. Use shadcn/ui form components.
```

**Done when:** As staff you can add a doctor with hours. As a patient, /staff redirects you away and RLS blocks direct writes.

### 6. Free slots and booking

Turn weekly hours into real times in the clinic time zone, remove busy times (busy_slots RPC) and past times, and show the next 14 days.

Prompt:

```text
Build /book: pick a doctor, then show the next 14 days with free times as buttons. Generate slots from availability + slot_minutes in CLINIC_TIMEZONE, remove past times and times returned by rpc('busy_slots'). Booking inserts an appointment in a Server Action with an optional reason. If the insert fails with code 23505, show "That time was just taken, pick another" and refresh the slots.
```

**Done when:** Two browsers trying the same slot: one books, the other sees the friendly message.

### 7. My appointments

Patients see upcoming and past appointments and can cancel upcoming ones.

Prompt:

```text
Build /appointments for patients: upcoming first (doctor, date, time in clinic time zone, reason), then past. A Cancel button on upcoming ones sets status to cancelled after a confirm step. The cancelled slot must become free again on /book.
```

**Done when:** Cancelling frees the slot immediately for everyone.

### 8. Staff: today's schedule

A day view with one column per doctor, so the front desk can see who is coming.

Prompt:

```text
Build /staff/schedule: date picker (default today), one column per active doctor, rows per slot, booked slots show patient name, phone and reason. Staff can mark an appointment done or cancelled. Make it readable on a tablet.
```

**Done when:** Staff see every booking for the day; patients still only see their own (test with two accounts).

### 9. Check security and deploy

Prove the privacy rules with two patient accounts, then deploy.

Prompt:

```text
Write a checklist and walk me through testing it: patient A cannot see patient B's appointments (UI and direct Supabase queries), patients cannot change their role, patients cannot book in the past. Then prepare for Vercel: list the env vars and add the production URL to Supabase Auth → URL configuration.
```

**Done when:** All checks pass and the app runs on your Vercel URL.

## Assets

### CLAUDE.md — Project root. Cursor users: save as AGENTS.md.

```
# CLAUDE.md — Clinic booking app

## Stack
- Next.js (App Router, TypeScript) from the official `with-supabase` starter
- Supabase: Postgres, Auth, Row Level Security
- Tailwind CSS + shadcn/ui (already in the starter)

## Rules
- Read `supabase/schema.sql` before touching data. Never change the schema without asking.
- All data access goes through the Supabase client from `lib/supabase` (server client in Server Components, Route Handlers and Server Actions).
- Security lives in RLS. Never use the service role key in the browser or in this app.
- Patients see free times through the `busy_slots` RPC, never through other patients' appointments.
- Times: store `timestamptz`. Convert weekly hours to real times in the clinic's time zone `CLINIC_TIMEZONE` (e.g. `Asia/Kolkata`).
- Double booking is blocked by a unique index. Catch error code `23505` and show "That time was just taken, pick another".
- Keep components small. Server Actions for writes. Show loading and error states.
- After each step: run `npm run build` and fix every error before continuing.

## Done means
- Patient: sign up → pick doctor → pick a free time → see it under "My appointments" → cancel.
- Staff: manage doctors and weekly hours, see today's schedule per doctor.
```

### supabase-schema.sql — Tables, double-booking guard and RLS policies. Save as supabase/schema.sql.

```
-- Booking App for Clinics: run this in Supabase → SQL Editor.

-- One profile per user, created automatically on sign-up.
create table public.profiles (
  id uuid primary key references auth.users (id) on delete cascade,
  full_name text,
  phone text,
  role text not null default 'patient' check (role in ('patient', 'staff')),
  created_at timestamptz not null default now()
);

create table public.doctors (
  id bigint generated always as identity primary key,
  name text not null,
  specialty text,
  slot_minutes int not null default 30 check (slot_minutes between 10 and 120),
  active boolean not null default true
);

-- Weekly working hours, e.g. Monday 09:00–13:00 (0 = Sunday).
create table public.availability (
  id bigint generated always as identity primary key,
  doctor_id bigint not null references public.doctors (id) on delete cascade,
  weekday int not null check (weekday between 0 and 6),
  start_time time not null,
  end_time time not null,
  check (end_time > start_time)
);

create table public.appointments (
  id bigint generated always as identity primary key,
  doctor_id bigint not null references public.doctors (id),
  patient_id uuid not null references public.profiles (id) on delete cascade,
  starts_at timestamptz not null,
  ends_at timestamptz not null,
  reason text,
  status text not null default 'booked' check (status in ('booked', 'cancelled', 'done')),
  created_at timestamptz not null default now(),
  check (ends_at > starts_at)
);

-- The database itself prevents double booking (cancelled bookings free the slot).
create unique index appointments_no_double_booking
  on public.appointments (doctor_id, starts_at)
  where status <> 'cancelled';

create or replace function public.is_staff()
returns boolean language sql stable security definer set search_path = public as $$
  select exists (select 1 from public.profiles where id = auth.uid() and role = 'staff');
$$;

create or replace function public.handle_new_user()
returns trigger language plpgsql security definer set search_path = public as $$
begin
  insert into public.profiles (id, full_name) values (new.id, new.raw_user_meta_data ->> 'full_name');
  return new;
end;
$$;

create trigger on_auth_user_created
  after insert on auth.users
  for each row execute function public.handle_new_user();

-- Patients may see which times are taken, but never who booked them.
create or replace function public.busy_slots(p_doctor bigint, p_from timestamptz, p_to timestamptz)
returns table (starts_at timestamptz) language sql stable security definer set search_path = public as $$
  select a.starts_at from public.appointments a
  where a.doctor_id = p_doctor and a.status <> 'cancelled'
    and a.starts_at >= p_from and a.starts_at < p_to;
$$;
grant execute on function public.busy_slots(bigint, timestamptz, timestamptz) to authenticated;

alter table public.profiles enable row level security;
alter table public.doctors enable row level security;
alter table public.availability enable row level security;
alter table public.appointments enable row level security;

create policy "Read own profile (staff read all)" on public.profiles
  for select to authenticated using (id = auth.uid() or public.is_staff());
create policy "Update own profile, never own role" on public.profiles
  for update to authenticated using (id = auth.uid())
  with check (id = auth.uid() and role = (select p.role from public.profiles p where p.id = auth.uid()));

create policy "Signed-in users see doctors" on public.doctors
  for select to authenticated using (true);
create policy "Staff manage doctors" on public.doctors
  for all to authenticated using (public.is_staff()) with check (public.is_staff());

create policy "Signed-in users see hours" on public.availability
  for select to authenticated using (true);
create policy "Staff manage hours" on public.availability
  for all to authenticated using (public.is_staff()) with check (public.is_staff());

create policy "Patients see own, staff see all" on public.appointments
  for select to authenticated using (patient_id = auth.uid() or public.is_staff());
create policy "Patients book for themselves" on public.appointments
  for insert to authenticated with check (patient_id = auth.uid() and status = 'booked' and starts_at > now());
create policy "Patients cancel own, staff update any" on public.appointments
  for update to authenticated using (patient_id = auth.uid() or public.is_staff())
  with check (public.is_staff() or (patient_id = auth.uid() and status = 'cancelled'));

-- Make yourself staff after signing up once:
-- update public.profiles set role = 'staff'
--   where id = (select id from auth.users where email = 'you@example.com');
```

### Next.js + Supabase starter — Official starter with cookie-based auth.

https://github.com/vercel/next.js/tree/canary/examples/with-supabase

### Supabase MCP (read-only) — Lets your AI read the schema. Read-only and scoped to one project.

Source: https://wonly.dhfm.in/builder-4392ac/clinic-booking-app/supabase-mcp-read-only/

Review this config before installing: it runs a command on your machine.

```json
{
    "mcpServers": {
        "supabase": {
            "command": "npx",
            "args": [
                "-y",
                "@supabase/mcp-server-supabase@latest",
                "--read-only",
                "--project-ref=YOUR_PROJECT_REF"
            ],
            "env": {
                "SUPABASE_ACCESS_TOKEN": "YOUR_PERSONAL_ACCESS_TOKEN"
            }
        }
    }
}
```

### Supabase Row Level Security guide — Read once: it is what keeps patient data private.

https://supabase.com/docs/guides/database/postgres/row-level-security


## Build instructions by tool

### Claude Code

Open a terminal in an empty folder, run `claude`, then paste the prompt.

```text
Build the "Booking App for Clinics" app from the OwnlyPlans blueprint at https://wonly.dhfm.in/builder-4392ac/clinic-booking-app/.
Stack: Next.js, shadcn/ui, Supabase, Tailwind.
Follow the plan's steps in order: Create the project from the official starter → Add the project rules and the Supabase MCP → Create the database → Onboarding and roles → Staff: doctors and weekly hours → Free slots and booking → My appointments → Staff: today's schedule → Check security and deploy.
Download the full plan (Markdown) from the link above and keep it in the project root as PLAN.md.
Ask me before adding anything not in the plan. (Prepared for Claude Code.)
```

### Cursor

Open an empty folder in Cursor, start an Agent chat, then paste the prompt.

```text
Build the "Booking App for Clinics" app from the OwnlyPlans blueprint at https://wonly.dhfm.in/builder-4392ac/clinic-booking-app/.
Stack: Next.js, shadcn/ui, Supabase, Tailwind.
Follow the plan's steps in order: Create the project from the official starter → Add the project rules and the Supabase MCP → Create the database → Onboarding and roles → Staff: doctors and weekly hours → Free slots and booking → My appointments → Staff: today's schedule → Check security and deploy.
Download the full plan (Markdown) from the link above and keep it in the project root as PLAN.md.
Ask me before adding anything not in the plan. (Prepared for Cursor.)
```

### Lovable

Start a new Lovable project and paste the prompt as your first message.

```text
Build the "Booking App for Clinics" app from the OwnlyPlans blueprint at https://wonly.dhfm.in/builder-4392ac/clinic-booking-app/.
Stack: Next.js, shadcn/ui, Supabase, Tailwind.
Follow the plan's steps in order: Create the project from the official starter → Add the project rules and the Supabase MCP → Create the database → Onboarding and roles → Staff: doctors and weekly hours → Free slots and booking → My appointments → Staff: today's schedule → Check security and deploy.
Download the full plan (Markdown) from the link above and keep it in the project root as PLAN.md.
Ask me before adding anything not in the plan. (Prepared for Lovable.)
```

## Definition of done

- Every build step is complete and the app runs locally.
- The main user flow works end to end.
- Share your build on OwnlyPlans with a demo link, build time and tool.
