Build a full clinic management system with V0 using Next.js, Supabase, Stripe, Resend, and shadcn/ui. Features patient registration, appointment scheduling with conflict detection, SOAP clinical notes, prescription management, invoice generation with Stripe payments, and automated appointment reminders via Vercel Cron — takes about 2-4 hours.
What you're building
Small clinics and private practices need an integrated system covering patient records, appointment scheduling, clinical notes, prescriptions, and billing. Off-the-shelf EHR systems are expensive and rigid.
V0 generates the complex multi-tab patient charts, appointment calendars, and billing interfaces from prompts. Supabase handles the relational data with strict RLS for provider-scoped access. Stripe manages patient invoice payments, and Resend sends automated appointment reminders.
The architecture uses Server Components for data-heavy pages, Supabase RPC for conflict detection on appointment booking, react-hook-form with zod for validated SOAP note entry, Stripe for invoice payment, and Vercel Cron for scheduled reminder emails.
Final result
A clinic management system with patient registration, appointment scheduling, SOAP notes, prescriptions, invoice billing with Stripe, and automated email reminders.
Tech stack
Prerequisites
- A V0 account (Premium or higher recommended)
- A Supabase project (free tier works — connect via V0's Connect panel)
- A Stripe account (test mode works — connect via Vercel Marketplace)
- A Resend account (free tier: 100 emails/day)
- Understanding of clinic workflows (appointments, notes, billing)
Build steps
Set up the database schema for patients, appointments, and billing
Create the Supabase schema for patients, providers, appointments with status tracking, clinical notes in SOAP format, prescriptions, and invoices with line items.
1// Paste this prompt into V0's AI chat:2// Build a patient management system. Create a Supabase schema:3// 1. patients: id (uuid PK), user_id (uuid FK to auth.users NULL), first_name (text), last_name (text), date_of_birth (date), gender (text), phone (text), email (text), address (text), insurance_provider (text), insurance_id (text), blood_type (text), allergies (text[]), emergency_contact (jsonb), created_at (timestamptz)4// 2. providers: id (uuid PK), user_id (uuid FK to auth.users), first_name (text), last_name (text), specialty (text), license_number (text), schedule_config (jsonb)5// 3. appointments: id (uuid PK), patient_id (uuid FK to patients), provider_id (uuid FK to providers), start_time (timestamptz), end_time (timestamptz), status (text DEFAULT 'scheduled' CHECK IN 'scheduled','checked_in','in_progress','completed','cancelled','no_show'), reason (text), notes (text)6// 4. clinical_notes: id (uuid PK), appointment_id (uuid FK to appointments), provider_id (uuid FK to providers), subjective (text), objective (text), assessment (text), plan (text), created_at (timestamptz)7// 5. prescriptions: id (uuid PK), patient_id (uuid FK to patients), provider_id (uuid FK to providers), appointment_id (uuid FK to appointments), medication (text), dosage (text), frequency (text), duration (text), refills (int DEFAULT 0), status (text DEFAULT 'active')8// 6. invoices: id (uuid PK), patient_id (uuid FK to patients), appointment_id (uuid FK to appointments), amount_cents (int), insurance_covered_cents (int), patient_owes_cents (int), status (text DEFAULT 'pending'), stripe_payment_id (text), created_at (timestamptz)9// 7. invoice_items: id (uuid PK), invoice_id (uuid FK to invoices), description (text), amount_cents (int), cpt_code (text)10// Add RLS so providers see only their patients. Create an RPC function check_appointment_conflict(provider_uuid, start_ts, end_ts) that returns true if an overlapping appointment exists.11// Generate SQL and types.Pro tip: Use V0's Git panel to push code to GitHub where a clinic developer can review sensitive healthcare code changes before deploying to production.
Expected result: All tables created with RLS policies scoped to provider access. The conflict detection RPC prevents double-booking.
Build the patient registry and chart pages
Create the patient list with search and the patient chart page with tabbed views for demographics, appointments, clinical notes, prescriptions, and billing.
1// Paste this prompt into V0's AI chat:2// Create patient management pages:3// 1. app/patients/page.tsx — patient registry Table with columns: name, DOB, phone, insurance Badge, last visit date. Add search Input that filters by name or phone. 'Add Patient' Button opens Dialog with registration form.4// 2. app/patients/[id]/page.tsx — patient chart with Tabs:5// - Demographics: personal info, insurance details, allergies (Badge list), emergency contact6// - Appointments: Table of past and upcoming appointments with status Badge, link to appointment detail7// - Notes: clinical notes list with date, provider, and accordion for SOAP content8// - Prescriptions: Table with medication, dosage, frequency, status Badge (active=green, expired=red), refills remaining9// - Billing: invoices Table with amount, insurance coverage, patient balance, payment status Badge10// Use shadcn/ui Tabs, Table, Badge, Dialog, Form, Input, Select, Textarea, Avatar.Expected result: The patient registry shows a searchable list. Clicking a patient opens a comprehensive chart with five tab sections.
Build the appointment scheduler and SOAP notes
Create the appointment calendar with time-slot views, conflict detection, and the SOAP note form for clinical documentation.
1// Paste this prompt into V0's AI chat:2// Create appointment and clinical note features:3// 1. app/appointments/page.tsx — daily/weekly calendar view for the logged-in provider. Show appointments as colored blocks based on status. 'New Appointment' Button opens Dialog with:4// - Patient Select (searchable), date Calendar picker, time Select, duration Select, reason Textarea5// - Server Action that calls check_appointment_conflict RPC before inserting. Show error Toast if conflict.6// 2. app/appointments/[id]/page.tsx — appointment detail Card: patient info, time, status. Status action Buttons: 'Check In', 'Start', 'Complete', 'No Show', 'Cancel'.7// - SOAP Note section: Form with Subjective Textarea, Objective Textarea, Assessment Textarea, Plan Textarea. Validate with zod (each field required, min 10 chars). Save via Server Action.8// - Add Prescription Button: Dialog with medication Input, dosage Input, frequency Select, duration Input, refills number Input.9// Use shadcn/ui Calendar, Form, Textarea, Select, Dialog, Button, Badge, Toast.Expected result: The appointment calendar shows the provider's schedule. New appointments check for conflicts. SOAP notes are validated and saved per appointment.
Build billing, payment, and appointment reminders
Create the invoice management system with Stripe payment collection, and the automated reminder endpoint triggered by Vercel Cron.
1import { NextResponse } from 'next/server'2import { createClient } from '@supabase/supabase-js'3import { Resend } from 'resend'45const supabase = createClient(6 process.env.SUPABASE_URL!,7 process.env.SUPABASE_SERVICE_ROLE_KEY!8)9const resend = new Resend(process.env.RESEND_API_KEY)1011export async function GET() {12 const tomorrow = new Date()13 tomorrow.setDate(tomorrow.getDate() + 1)14 const startOfDay = new Date(tomorrow.setHours(0, 0, 0, 0)).toISOString()15 const endOfDay = new Date(tomorrow.setHours(23, 59, 59, 999)).toISOString()1617 const { data: appointments } = await supabase18 .from('appointments')19 .select(`20 id, start_time, reason,21 patients!inner(first_name, last_name, email),22 providers!inner(first_name, last_name)23 `)24 .eq('status', 'scheduled')25 .gte('start_time', startOfDay)26 .lte('start_time', endOfDay)2728 if (!appointments?.length) {29 return NextResponse.json({ sent: 0 })30 }3132 const results = await Promise.allSettled(33 appointments.map((appt: any) =>34 resend.emails.send({35 from: 'clinic@yourdomain.com',36 to: appt.patients.email,37 subject: 'Appointment Reminder — Tomorrow',38 html: `<p>Hi ${appt.patients.first_name},</p>39 <p>This is a reminder that you have an appointment tomorrow at ${new Date(appt.start_time).toLocaleTimeString()} with Dr. ${appt.providers.last_name}.</p>40 <p>Reason: ${appt.reason || 'General visit'}</p>`,41 })42 )43 )4445 const sent = results.filter((r) => r.status === 'fulfilled').length46 return NextResponse.json({ sent, total: appointments.length })47}Expected result: The reminder endpoint sends emails for tomorrow's appointments. Vercel Cron triggers this daily at 8 AM.
Add invoice management with Stripe and deploy
Build the billing page with invoice creation, Stripe payment links, and the webhook handler for payment confirmation. Configure Vercel Cron and deploy.
1// Paste this prompt into V0's AI chat:2// Create billing features:3// 1. app/billing/page.tsx — invoice management Table: patient name, appointment date, total amount, insurance covered, patient owes, status Badge (pending=yellow, paid=green, overdue=red). Filter by status Select and date range.4// 2. 'Generate Invoice' Button on completed appointments: Dialog with invoice_items — add line items with description Input, CPT code Input, amount Input. Calculate insurance coverage (configurable percentage). Server Action creates invoice and invoice_items rows.5// 3. 'Send Payment Link' Button: Server Action creates Stripe Payment Link for patient_owes_cents and sends via Resend email.6// 4. app/api/webhooks/stripe/route.ts — handle payment_intent.succeeded: update invoice status to 'paid', store stripe_payment_id. Use request.text() for signature verification.7// 5. Configure vercel.json with cron: {"crons": [{"path": "/api/appointments/reminders", "schedule": "0 8 * * *"}]}8// Use shadcn/ui Table, Badge, Dialog, Input, Button, Separator.Pro tip: Configure Vercel Cron in vercel.json for the reminders endpoint. Set RESEND_API_KEY, STRIPE_SECRET_KEY, and STRIPE_WEBHOOK_SECRET in Vars without NEXT_PUBLIC_ prefix since they are all server-only.
Expected result: Invoices can be generated from completed appointments. Payment links are sent to patients. The webhook confirms payments. Reminders run daily. The app is deployed.
Complete code
1import { NextResponse } from 'next/server'2import { createClient } from '@supabase/supabase-js'3import { Resend } from 'resend'45const supabase = createClient(6 process.env.SUPABASE_URL!,7 process.env.SUPABASE_SERVICE_ROLE_KEY!8)9const resend = new Resend(process.env.RESEND_API_KEY)1011export async function GET() {12 const tomorrow = new Date()13 tomorrow.setDate(tomorrow.getDate() + 1)14 const start = new Date(tomorrow.setHours(0, 0, 0, 0)).toISOString()15 const end = new Date(tomorrow.setHours(23, 59, 59, 999)).toISOString()1617 const { data: appts } = await supabase18 .from('appointments')19 .select(`20 id, start_time, reason,21 patients!inner(first_name, last_name, email),22 providers!inner(first_name, last_name)23 `)24 .eq('status', 'scheduled')25 .gte('start_time', start)26 .lte('start_time', end)2728 if (!appts?.length) {29 return NextResponse.json({ sent: 0 })30 }3132 const results = await Promise.allSettled(33 appts.map((a: any) =>34 resend.emails.send({35 from: 'clinic@yourdomain.com',36 to: a.patients.email,37 subject: 'Appointment Reminder — Tomorrow',38 html: `<p>Hi ${a.patients.first_name},</p>39 <p>Reminder: appointment tomorrow at40 ${new Date(a.start_time).toLocaleTimeString()}41 with Dr. ${a.providers.last_name}.</p>42 <p>Reason: ${a.reason || 'General visit'}</p>`,43 })44 )45 )4647 const sent = results.filter((r) => r.status === 'fulfilled').length48 return NextResponse.json({ sent, total: appts.length })49}Customization ideas
Patient portal with self-service
Add a patient-facing portal where patients can view their upcoming appointments, download visit summaries, and request prescription refills.
SMS reminders via Twilio
Add SMS appointment reminders alongside email by integrating Twilio in the reminders endpoint for patients who prefer text messages.
Lab results tracking
Add a lab_results table linked to patients and appointments. Upload lab reports to Supabase Storage and display results in the patient chart.
Provider analytics dashboard
Build a dashboard showing appointments per day, average visit duration, revenue trends, no-show rates, and popular visit reasons with Recharts.
Common pitfalls
Pitfall: Not checking for appointment conflicts before booking
How to avoid: Use a Supabase RPC function that checks for overlapping appointments (start_time < new_end AND end_time > new_start) with the same provider before inserting. Return an error if a conflict exists.
Pitfall: Exposing patient data without provider-scoped RLS
How to avoid: Create RLS policies that scope data access: providers see only their own patients (via appointments or a provider-patient junction), and patients see only their own records via user_id matching.
Pitfall: Using request.json() for the Stripe webhook body
How to avoid: Use request.text() and pass the raw body to stripe.webhooks.constructEvent().
Best practices
- Use a Supabase RPC function for appointment conflict detection to prevent double-booking
- Implement provider-scoped RLS policies on every table containing patient data
- Validate SOAP notes with zod schemas requiring minimum content length per field
- Use react-hook-form for all clinical forms to handle complex validation and dirty state tracking
- Always use request.text() for Stripe webhook signature verification
- Configure Vercel Cron in vercel.json for automated appointment reminders
- Store RESEND_API_KEY, STRIPE_SECRET_KEY, and STRIPE_WEBHOOK_SECRET in Vars without NEXT_PUBLIC_ prefix
- Use V0's Git panel to push to GitHub for code review before deploying healthcare-sensitive changes
AI prompts to try
Copy these prompts to build this project faster.
I'm building a patient management system with Next.js App Router, Supabase, Stripe, and Resend. I need an appointment reminder endpoint at app/api/appointments/reminders/route.ts that queries tomorrow's scheduled appointments, joins patient email and provider name, and sends reminder emails via Resend. It should use Promise.allSettled to handle partial failures and return a count of sent emails. This endpoint will be triggered by Vercel Cron daily at 8 AM.
Create a SOAP note entry form component using react-hook-form and zod. It has four Textarea fields: Subjective (patient's description), Objective (exam findings), Assessment (diagnosis), and Plan (treatment plan). Each field is required with a minimum of 10 characters. Show character count below each field. Include a Save Button that submits via a Server Action and shows a success Toast. Use shadcn/ui Form, Textarea, Button, and Toast.
Frequently asked questions
How does appointment conflict detection work?
A Supabase RPC function checks for overlapping appointments by querying where an existing appointment's start_time is before the new end_time AND the existing end_time is after the new start_time for the same provider. If any overlap exists, the function returns true and the Server Action shows an error Toast instead of creating the appointment.
How are SOAP notes validated?
The SOAP note form uses react-hook-form with a zod schema requiring all four fields (Subjective, Objective, Assessment, Plan) with a minimum of 10 characters each. This ensures clinical documentation meets a baseline quality before saving.
How does patient data security work?
Supabase RLS policies scope all data access. Providers can only see patients they have appointments with. Patients can only see their own records. The service role key (used only in server-side code) bypasses RLS for administrative operations like reminder emails.
How do appointment reminders work?
A Vercel Cron job triggers the reminders endpoint daily at 8 AM. The endpoint queries tomorrow's scheduled appointments, joins patient emails and provider names, and sends reminder emails via Resend. Promise.allSettled ensures one failed email does not block the others.
Do I need a paid V0 plan?
Premium ($20/month) is recommended. The patient management system has many complex pages (patient chart, appointment calendar, SOAP notes, billing) that require multiple prompts to generate.
How do I deploy the system?
Click Share in V0, then Publish to Production. Set STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, RESEND_API_KEY, and Supabase keys in the Vars tab. Add the cron configuration to vercel.json. Register the Stripe webhook URL for payment_intent.succeeded.
Can RapidDev help build a custom clinic management system?
Yes. RapidDev has built over 600 apps including healthcare platforms with HIPAA-compliant data handling, telehealth integration, and multi-location support. Book a free consultation to discuss your clinic's requirements.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation