Loading repository data…
Loading repository data…
abdulrehmankz1 / repository
Multi-tenant clinic management SaaS for small clinics. Staff, patients, appointments, walk-ins, scheduling, and strict tenant isolation built with Payload CMS, Next.js, and MongoDB.
A transparent discovery signal based on current public GitHub metadata.
This score does not audit code, security, maintainers, documentation quality, or suitability. Verify the repository and its current documentation before adoption.
A multi-tenant clinic management system built with Payload CMS 3, Next.js 16 and MongoDB. Many clinics share one deployment, but each clinic sees only its own data — built around one non-negotiable rule: a clinic can never see or touch another clinic's data. v1 ships the multi-tenant core (staff & roles, patients, appointments with a double-booking guard, walk-in tokens, dashboard); v2 closes the clinical loop — visits & prescriptions, billing with payments, printable prescription & receipt, a merged patient timeline and owner revenue reporting; v3 turns it into a real SaaS — self-serve clinic signup, plans & usage limits with an upgrade workflow, timezone-aware reminders, and an append-only audit log.
⭐ Like this project? Give it a star and feel free to fork it — it really helps and keeps the project going!
A single end-to-end run as a clinic owner, covering the everyday front-desk flow: booking three ways (scheduled, walk-in, and a brand-new patient added inline), seeing where bookings land on the day view, registering a patient, adding a doctor, and reviewing clinic settings.
▶ Full quality: watch the MP4
What it shows
The v3 reminder flow: a per-appointment WhatsApp reminder sent straight from the day
view (a wa.me deep link with the message prefilled — zero API cost), and the
daily digest email a clinic owner receives at 7 am their clinic's local time.
▶ Full quality: watch the MP4
Small clinics — one to a few doctors, a receptionist — still run on paper appointment registers, patient histories in loose files, and a cash drawer. Existing hospital systems are expensive, complex, and assume IT staff. There's a gap for a cheap, simple, multi-clinic tool a receptionist can learn in half an hour.
superAdmin (platform), and per-clinic owner, doctor,
receptionist, each with its own access.P-0001); allergy banner; same-number dedupe warning (families share phones).scheduled → checked-in → completed / cancelled / no-show) and a
double-booking guard that rejects overlapping slots.regular (set weekdays + a daily window, e.g.
Mon/Wed/Fri 4–6pm), on-call (any time), or by-appointment (visiting surgeon — hidden
from the auto finder). Bookings are blocked outside a regular doctor's window, and a
"which doctors are free at this time?" finder powers the call-in flow.T-01, T-02…)
per clinic per day, shown on the Day Rail.The loop appointment → consultation → prescription → invoice → payment, end to end.
totalAmount,
amountPaid, balanceDue and paymentStatus (unpaid → partial → paid) are always
computed in a hook — client-sent values are ignored. Overpayment is rejected, line items
lock after the first payment, and only an owner can void (with a reason); voided
invoices are frozen and excluded from revenue.@media print CSS and window.print() — no PDF library.slug/status stay super-admin only).Matab stops being an attended demo and starts behaving like a product: clinics sign themselves up, plans limit usage, the system communicates, and every sensitive action leaves a record.
/signup creates the clinic + owner + sample data
atomically in one transaction (any failure rolls everything back). Slugs are
auto-generated with -2/-3 suffixes on collision; a honeypot field, a minimum-fill-time
check and per-IP rate limiting (3/hour) keep bots out. New clinics queue as pending
until the super admin approves them, and the first login gets a 3-step welcome checklist
derived from live counts — no extra state stored.free (1 doctor, 50 patients) / clinic (5 doctors) / plus
(unlimited), defined in code (src/lib/plans.ts) and enforced in beforeChange hooks
with a clear PLAN_LIMIT error. Downgrade-safe: if a plan shrinks below current
usage, existing data stays readable and editable — only new creates are blocked.
Deactivated doctors don't count toward the limit./dashboard/plan
page with usage bars and plan cards; "Request upgrade" with an optional note; a
super-admin approve/decline queue on /super. The enforcement and approval workflow are
real — the payment gateway is a swappable last mile.auditLogs collection written only from server-side
hooks: REST create/update/delete is denied for everyone, super admin included.
Owners get a /dashboard/activity page with action filters; the super console gets a
per-tenant activity peek. An audit-write failure never breaks the business operation.CRON_SECRET, plus a per-appointment WhatsApp reminder
button — a wa.me deep link with the message prefilled. Zero API cost, works anywhere
WhatsApp does.Each clinic sets its own timezone, so "email the owner at 7 am" can't be a single
scheduled job. Instead the digest cron runs hourly, and each run sends only to
clinics whose local time is currently 7 — a clinic in Karachi and one in London both get
their digest at their own 7 am from the same cron. One clinic's email failure is logged
and skipped, never crashing the loop; without RESEND_API_KEY the whole feature quietly
switches off (graceful degradation).
The tenant wall lives in one place — access-control functions (src/access/) that
return a Mongo where constraint Payload merges into every query, e.g.
{ tenant: { equals: user.tenant } }. It is deny-by-default: any request that can't
positively determine a tenant returns false.
beforeChange hook force-sets tenant from the
logged-in user; whatever the client sends is overwritten. tenant is immutable after create.tests/int/isolation.int.spec.ts asserts cross-tenant
reads/writes fail and that planted foreign tenants are force-corrected.Two appointments for the same doctor conflict iff
existing.start < new.end && existing.end > new.start (touching edges don't conflict).
The check runs inside the request's MongoDB transaction, and a partial unique index
on (tenant, doctor, start) (active statuses only) is the deterministic backstop so two
simultaneous bookings for the same slot can't both win — one aborts. A race test
(Promise.all of two identical bookings) proves exactly one succeeds.
Nothing region-specific is hardcoded. Each clinic has its own currency and timezone
as settings (defaults PKR / Asia/Karachi). All money flows through
formatMoney(amount, tenant) and all times through formatTime(date, tenant). Pakistan is
the launch market, not a limitation.
| Layer | Choice |
|---|---|
| CMS / API / Auth | Payload CMS 3 |
| Web | Next.js 16 (App Router) + React 19 |
| DB | MongoDB (replica set — for transactions) |
| Styling | Tailwind CSS v4 + shadcn/ui (Base UI) + lucide-react |
| Tests | Vitest (Payload Local API integration tests) |
pnpm install
cp .env.example .env # then fill PAYLOAD_SECRET
docker compose up -d # MongoDB as a single-node replica set (rs0)
pnpm seed # 3 demo clinics, staff, patients, appointments, visits, invoices & payments
pnpm dev # h