MediFlow is a role-based clinic operations platform built with Next.js App Router, TypeScript, Prisma, MongoDB, Tailwind CSS, and shadcn/ui. It covers the front-desk to consultation to billing workflow for small clinics and multi-user teams.
This README is a current-state guide based on the checked-in codebase as of March 9, 2026.
- Overview
- What the App Does
- Roles and Access
- End-to-End Workflow
- Tech Stack
- Project Structure
- Getting Started
- Environment Variables
- Database Setup
- Seed Data and Demo Credentials
- Available Scripts
- Application Routes
- API Overview
- Data Model
- Authentication and Security
- Realtime Behavior
- Deployment Notes
- Known Constraints
- Troubleshooting
MediFlow is designed for three runtime roles:
admindoctorreceptionist
The application currently supports:
- Patient registration and profile management
- Appointment booking and queue transitions
- Clinical encounters and prescription authoring
- Billing, checkout, payment collection, voids, and refunds
- Reports and operational dashboards
- User and service management for admins
- Register new patients
- Search and update patient records
- Schedule appointments
- Move patients into queue
- Create bills and collect payments
- View queue and daily appointments
- Start consultations
- Record encounters
- Create prescriptions
- Complete appointments and hand off to billing
- Review reports and patient history
- Everything receptionist and doctor roles can do, plus:
- Create the initial admin account through
/makeadmin - Manage users
- Manage services
- Access settings and reports
/dashboard/patients/add-patient/appointments/queue/billing
/dashboard/appointments/queue/patients/prescriptions/billing/reports
/dashboard/appointments/queue/patients/add-patient/billing/reports/settings/users/prescriptions
Notes:
- Route access is enforced in both
middleware.tsand the client auth context. - Doctors can access the billing screen, but billing write operations are still permission-restricted at the API layer.
- User signs in at
/. - Session cookie is issued after successful login.
- Front desk registers a patient or reuses an existing patient record.
- Appointment is created and assigned to a doctor.
- Patient is moved through queue states such as
Scheduled,Arrived,InQueue,InProgress, andCompleted. - Doctor performs consultation and records encounter details.
- Doctor creates a prescription, or explicitly completes without one where allowed by the workflow.
- Billing creates or finalizes a bill from the encounter or appointment.
- Payment is collected and tracked.
- Admins and doctors review operational reports.
- Next.js
15.2.4with App Router - React
19 - TypeScript
5 - Prisma
5with MongoDB datasource - Tailwind CSS
3 - shadcn/ui and Radix UI primitives
josefor JWT session handlingbcryptjsfor password hashing- Recharts for reporting visuals
app/
(dashboard)/ Protected dashboard routes
api/ Route handlers for auth, patients, billing, etc.
layout.tsx Root layout
page.tsx Login page
makeadmin/ First-admin bootstrap flow
components/
appointments/ Appointment UI
auth/ Login and auth-related UI
billing/ Billing UI
dashboard/ Overview widgets
doctor/ Queue and prescription flows
layout/ Sidebar, theme toggle, shared shell
patients/ Patient forms and profile views
reports/ Reporting UI
settings/ Settings screens
users/ User management UI
ui/ Shared design system components
config/ Brand, roles, routes, theme, and feature config
contexts/ Auth context used by client-side UI
docs/ Internal project docs and audits
lib/ Auth, DB, RBAC, API client, realtime helpers
prisma/ Prisma schema and seed
public/ Static assets
services/ Supporting service-layer code
types/ Shared TypeScript types
- Node.js 20 LTS recommended
- npm
- A running MongoDB database or MongoDB Atlas cluster
Next.js 15 supports Node 18.18+, but Node 20 is the safer default for local development.
npm installCreate or update .env.local with the required keys listed below.
npm run devOpen http://localhost:3000.
The repository currently reads these environment variables:
| Variable | Required | Purpose |
|---|---|---|
DATABASE_URL |
Yes | MongoDB connection string used by Prisma |
JWT_SECRET_KEY |
Yes | Primary JWT signing secret for session cookies |
NEXTAUTH_SECRET |
Optional | Fallback JWT secret if JWT_SECRET_KEY is not set |
ADMIN_BOOTSTRAP_TOKEN |
Recommended | Required to create the first admin through /makeadmin |
COOKIE_SECURE |
Recommended | Set to true in HTTPS environments so auth cookies are marked secure |
NEXT_PUBLIC_APP_URL |
Recommended | Used in middleware for allowed CORS origins |
MOCK_LOGIN |
Optional | Present in env files; legacy/debug flag |
NEXT_PUBLIC_MOCK_LOGIN |
Optional | Present in env files; legacy/debug flag |
NEXT_PUBLIC_BRAND_LOGO_SRC |
Optional | Overrides logo source from config/brand.ts |
Example:
DATABASE_URL="mongodb+srv://<user>:<password>@<cluster>/<db>?retryWrites=true&w=majority"
JWT_SECRET_KEY="replace-with-a-long-random-secret"
ADMIN_BOOTSTRAP_TOKEN="replace-with-bootstrap-token"
COOKIE_SECURE="false"
NEXT_PUBLIC_APP_URL="http://localhost:3000"The Prisma datasource is configured for MongoDB in prisma/schema.prisma.
Generate Prisma client and sync the schema:
npx prisma generate
npx prisma db pushSeed the database:
npx prisma db seedWhat the seed creates:
- 1 admin
- 2 doctors
- 1 receptionist
- Sample patients
- Active services
- Sample appointments
- At least one encounter and prescription
- Sample billing data
- Audit log entries
Seeded credentials from prisma/seed.ts:
| Role | Password | |
|---|---|---|
| Admin | admin@mediflow.com |
Admin@123 |
| Doctor | doctor@mediflow.com |
Doctor@123 |
| Doctor | doctor2@mediflow.com |
Doctor@123 |
| Receptionist | reception@mediflow.com |
Reception@123 |
If you do not seed the database, create the first admin at /makeadmin using ADMIN_BOOTSTRAP_TOKEN.
Defined in package.json:
| Script | Command | Purpose |
|---|---|---|
npm run dev |
next dev |
Start local development server |
npm run build |
next build |
Create production build |
npm run start |
next start |
Run production server |
npm run lint |
next lint |
Run linting |
npm run typecheck |
tsc --noEmit |
Type-check without emitting |
npm run check |
tenancy + error checks | Release gate. Runs both self-checks below |
npm run check:tenancy |
scripts/check-tenant-scope.ts |
Proves clinic isolation rules hold. No database needed |
npm run check:errors |
scripts/check-error-disclosure.ts |
Proves API errors do not leak internal detail. No database needed |
npm run check:api |
scripts/check-api-isolation.ts |
Integration check against a running app: cross-tenant access and the billing path |
npm run e2e |
scripts/e2e-smoke.ts |
Launch gate. Walks the whole clinic journey over HTTP against a running app |
npm run db:backfill |
prisma/backfill-clinic-id.ts |
One-shot clinicId backfill for pre-multi-tenant databases |
npm run clean |
rm -rf .next .next-dev |
Remove Next.js build artifacts |
check:tenancy and check:errors need no database and run in CI on every push.
check:api needs the app running and a reachable database:
npm run dev # terminal 1
npm run check:api # terminal 2npm run e2e is the end-to-end smoke test. It signs in as all three roles and
walks the real journey — register a patient, book, queue, consult, prescribe,
complete, raise the bill, take a part payment, settle the balance — asserting
the RBAC and money rules along the way. Point it at any environment:
BASE_URL=http://localhost:3000 npm run e2eIt expects the seeded accounts from prisma/seed.ts, creates its own patient
with a unique phone, and is safe to re-run (it waits out the login throttle
rather than asking for it to be relaxed).
/login page/makeadminfirst-admin bootstrap page/reset-passwordpassword reset (reached via an admin-issued link)
/dashboard/patients/patients/[id]/add-patient/appointments/queue/prescriptions/billing/reports/settings/users
POST /api/auth/loginPOST /api/auth/logoutGET /api/auth/mePOST /api/auth/registerPOST /api/auth/makeadminGET /api/auth/doctors
GET /api/patientsPOST /api/patientsGET /api/patients/:idPATCH /api/patients/:id
GET /api/appointmentsPOST /api/appointmentsPATCH /api/appointmentsGET /api/appointments/:idPATCH /api/appointments/:idDELETE /api/appointments/:id
GET /api/encountersPOST /api/encountersGET /api/encounters/:idPATCH /api/encounters/:id
GET /api/prescriptionsPOST /api/prescriptionsGET /api/prescriptions/:idPATCH /api/prescriptions/:id
GET /api/billingPOST /api/billingGET /api/billing/:idPATCH /api/billing/:idPOST /api/billing/checkoutPOST /api/billing/:id/voidPOST /api/payments/:id/refund
GET /api/servicesPOST /api/servicesPATCH /api/services/:idGET /api/usersPATCH /api/users/:id
GET /api/dashboard/statsGET /api/realtime/appointments
Primary Prisma models:
UserPatientAppointmentAppointmentStatusTransitionEncounterPrescriptionPrescriptionItemBillBillItemAuditLogService
Key relationships:
- A patient can have many appointments, encounters, prescriptions, and bills.
- An appointment belongs to one patient and one doctor.
- An encounter links appointment, patient, and doctor.
- A prescription links encounter, patient, and doctor.
- A bill belongs to a patient and can optionally link to an encounter.
The current auth stack is cookie-based and JWT-backed.
- Session cookie name:
session - Token library:
jose - Password hashing:
bcryptjs - Session lifetime: 24 hours
- Route enforcement:
middleware.ts - API authorization:
lib/rbac.ts
Implemented security behavior:
- Protected dashboard routes by role
- API permission checks by role and action
- Rate limiting on sensitive auth endpoints
- Security headers on middleware responses
- Request ID propagation with
x-request-id - Audit logging for important actions
Current rate-limit config in middleware:
/api/auth/login: 5 requests per minute/api/auth/register: 3 requests per minute/api/auth/makeadmin: 5 requests per minute
Appointments are updated through a combination of:
- Server-Sent Events on
/api/realtime/appointments - In-memory realtime event fan-out in the backend
- Polling and retry behavior in the client API layer
This supports queue refreshes and status updates without relying only on manual reloads.
Every record that belongs to a clinic carries a clinicId, and isolation is
enforced centrally rather than per query:
requireAuth(lib/rbac.ts) resolves the caller's clinic and binds it to the request viaAsyncLocalStorage(lib/tenant-context.ts).- A Prisma client extension (lib/db.ts) injects
clinicIdinto thewhereof every read, update and delete on a tenant model, and stamps it onto every create. Routes cannot forget to scope a query. - It fails closed: touching a tenant model with no clinic in scope throws rather than returning unscoped rows.
- Genuinely cross-tenant work (login by email, admin bootstrap) must opt out
explicitly with
withoutTenantScope().
npm run check proves these rules hold and is a release gate.
Two limits worth knowing: a user belongs to exactly one clinic, and there is no platform-level super-admin that can see across clinics.
clinicId is a required field, so documents written before multi-tenancy will
fail to read until they are backfilled. On an existing deployment, run this
once before starting the new build:
npm run db:backfill -- --dry-run # report what would change
npm run db:backfill # apply; defaults to clinic_1
CLINIC_ID=acme npm run db:backfill # or name the clinic explicitlyThe script is idempotent and only touches documents with no clinicId. It also
drops the old global unique index on patients.phone — that constraint is now
per clinic (@@unique([clinicId, phone])), so two clinics can each register the
same phone number. Run npx prisma db push afterwards to create the new index.
Before deploying:
- Set production values for
DATABASE_URL,JWT_SECRET_KEY, andNEXT_PUBLIC_APP_URL. - Run
npx prisma generate. - Run
npx prisma db pushagainst the target database. - Existing databases only: run
npm run db:backfill(see above). - Run
npm run check— tenant isolation and error-disclosure gates. - Build the app with
npm run build. - Start it with
npm run start. - Run
BASE_URL=<deployed-url> npm run e2eagainst the running deployment. This is the last gate: it proves the whole reception → consultation → billing path actually works in that environment, not just that it compiled.
Recommended production checks:
- Serve over HTTPS. Session cookies are
Secureby default;COOKIE_SECURE=falseexists only for local http development and must never be set in production. - Rotate bootstrap and JWT secrets
- Restrict MongoDB network access
- Seed only in non-production or controlled environments
- Verify CORS origin matches the deployed app URL
The login page shows an autofill panel for the seeded E2E accounts. It is gated
on NODE_ENV !== "production", and the credentials sit inside that conditional
so the bundler drops them entirely from a production build — a render-only guard
would still have shipped the passwords in the JS bundle. To confirm after a
build:
grep -r "AdminE2E123" .next/static .next/server # must return nothingAuth endpoints are throttled using a MongoDB-backed counter
(RateLimitCounter), so limits survive serverless cold starts and are shared
across instances. This runs in the API routes, not middleware: middleware
executes on the edge runtime, where Prisma cannot connect. Login is limited per
IP and per email address. If the database is unreachable the limiter fails
open and logs ratelimit.unavailable rather than locking everyone out.
No mail transport is configured, so reset is admin-initiated: Users → ⋯ →
Create password reset link produces a single-use link (valid one hour) for the
admin to hand to the staff member. Only the SHA-256 hash of the token is stored.
Completing a reset sets passwordChangedAt, which invalidates every session
issued earlier — so a reset evicts an attacker who already holds a cookie.
Adding self-service "forgot password" needs only a mailer plus a route that looks the user up by email; the token machinery already exists in lib/password-reset.ts.
These are current implementation constraints, not README omissions:
- Page routes are still gated client-side through
AuthContext; the API is the real enforcement boundary. A direct URL may flash UI before redirecting, but no data is served without a valid session. - A user belongs to exactly one clinic, and there is no cross-clinic super-admin role.
- There is no subscription, licensing, or tenant self-provisioning layer — clinics are created by an operator running the admin bootstrap.
- Healthcare compliance (DPDP/HIPAA posture, retention policy, patient data export, documented backup and restore) is not addressed in the codebase and must be handled before selling into regulated use.
- Doctors can open the billing route, but billing write actions are still blocked by API permissions.
- Admins can access prescriptions directly, but the admin sidebar does not expose that route.
- The add-patient UI captures more fields than are fully persisted end-to-end in all flows.
- Some configuration and feature-flag scaffolding exists but is not yet fully wired into runtime behavior.
- A public
/registerpage does not exist even though registration capability exists at the API level for authorized users.
- Check that
JWT_SECRET_KEYis set. - Confirm cookies are enabled in the browser.
- If running over HTTP locally, use
COOKIE_SECURE=false.
- Confirm
ADMIN_BOOTSTRAP_TOKENis set in your environment. - Use the same token on
/makeadmin. - After at least one active admin exists, bootstrap is no longer public.
- Verify
DATABASE_URL. - Confirm the database allows inbound connections from your machine or host.
- Re-run
npx prisma generateandnpx prisma db push.
- Check role-route alignment in
middleware.tsandconfig/roles.ts. - Clear stale cookies and sign in again.
- Confirm the signed-in role has the required backend permission in
lib/rbac.ts. - Review server logs for validation failures and request IDs.
- Product audit:
docs/current-product-audit-report.md - Configuration guide:
config/README.md - Prisma schema:
prisma/schema.prisma - Seed script:
prisma/seed.ts