What We're Building
In this comprehensive guide, you will build a secure patient portal where users can check symptoms with AI assistance, book appointments, view medical records, and communicate with healthcare providers. This is a production-grade application that prioritizes security and data protection.
The portal includes these core features:
- AI Symptom Checker - Conversational interface powered by Claude for preliminary symptom guidance
- Appointment Booking - Calendar integration with provider availability and reminders
- Medical Records Access - Secure, encrypted storage and retrieval of health information
- Provider Messaging - Secure communication channel between patients and healthcare providers
- MFA Authentication - Multi-factor authentication for enhanced account security
Prerequisites
Before starting this guide, make sure you have the following:
- Strong knowledge of React/Next.js - You should be comfortable with React hooks, server components, and Next.js App Router
- Understanding of authentication and security - Familiarity with JWT, session management, and secure coding practices
- Familiarity with healthcare data considerations - Basic understanding of why health data requires special handling
- Free account: Supabase - For database and authentication (supabase.com)
- Free account: Vercel - For deployment (vercel.com)
- API access: Anthropic - For Claude API integration (console.anthropic.com)
Tech Stack Specification
Here is the technology stack we will use for this build, chosen specifically for security and healthcare application requirements:
| Layer | Technology | Why This Choice |
|---|---|---|
| Frontend | Next.js 14, TypeScript, Tailwind CSS | SSR for security, type safety for data handling |
| Backend | Node.js with Express | More control for health data handling and middleware |
| Database | Supabase PostgreSQL with encryption | Secure, encrypted at rest, row-level security |
| AI | Claude API (claude-3-sonnet) | Better reasoning capabilities for symptom analysis |
| Auth | Supabase Auth with MFA | Enhanced security with multi-factor authentication |
| Scheduling | Cal.com API or custom | Open-source scheduling with flexibility |
| Hosting | Vercel or Railway | Secure hosting with environment variable protection |
AI Agent Workflow
Here is how to leverage AI tools throughout this build to maximize productivity while maintaining security standards:
Claude Code - Core Development
Use Claude Code for setting up secure authentication flows, building the symptom checker logic, creating encrypted data storage patterns, and implementing audit logging. Claude Code excels at understanding security requirements and generating secure-by-default code.
# Example prompt for Claude Code
Create a Next.js 14 patient portal with:
- Secure authentication using Supabase Auth with MFA
- A symptom checker that uses Claude API to provide preliminary guidance
- Clear disclaimers that this is not medical advice
- Appointment booking functionality
- Encrypted patient notes storage
- Audit logging for all data access
Focus on security best practices:
- Server-side rendering for sensitive pages
- CSRF protection
- Rate limiting on API routes
- Input sanitization for all user inputs
v0.dev - UI Generation
Use v0.dev for generating patient dashboard layouts, symptom checker conversation UI, appointment calendar components, and medical history displays. The generated components provide a solid foundation that you can then secure and customize.
When using v0.dev for healthcare UIs, prompt for accessibility features explicitly. Healthcare applications often serve users with varying abilities, so WCAG compliance should be built in from the start.
Cursor - Development & Security
Use Cursor for security review and hardening, implementing data protection considerations, and testing authentication flows. Cursor's inline AI suggestions are particularly helpful for catching security issues during development.
Step-by-Step Build Guide
Phase 1: Secure Foundation
Start by setting up Next.js with strict TypeScript configuration. Security begins at the foundation level with proper type checking and secure defaults.
# Create Next.js project with TypeScript
npx create-next-app@latest patient-portal --typescript --tailwind --app
# Navigate to project
cd patient-portal
# Install security and healthcare-specific dependencies
npm install @supabase/supabase-js @supabase/auth-helpers-nextjs
npm install @anthropic-ai/sdk
npm install zod # For input validation
npm install nanoid # For secure ID generation
Configure environment variables securely. Never commit these to version control:
# .env.local (never commit this file)
NEXT_PUBLIC_SUPABASE_URL=your_supabase_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_anon_key
SUPABASE_SERVICE_ROLE_KEY=your_service_role_key
ANTHROPIC_API_KEY=your_claude_api_key
ENCRYPTION_KEY=your_32_character_encryption_key
Set up audit logging from day one. Every data access should be logged for security and compliance purposes.
Phase 2: Authentication System
Build a robust MFA-enabled authentication flow. Healthcare applications require stronger authentication than typical web apps.
// lib/supabase-client.ts - Secure client setup
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'
export const createSecureClient = () => {
return createClientComponentClient({
options: {
auth: {
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: true,
},
},
})
}
// Configure MFA requirement
export const MFA_REQUIRED = true
Key authentication features to implement:
- Multi-factor authentication using TOTP (Time-based One-Time Password)
- Secure session management with automatic refresh
- Strong password requirements (minimum 12 characters, complexity rules)
- Account lockout after failed attempts
- Session timeout for inactive users
Phase 3: Patient Dashboard
Create the main patient interface with secure data fetching. All patient data should be fetched server-side when possible.
// app/dashboard/page.tsx - Server component for security
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import { logDataAccess } from '@/lib/audit'
export default async function DashboardPage() {
const supabase = createServerComponentClient({ cookies })
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
redirect('/login')
}
// Log data access for audit trail
await logDataAccess({
userId: user.id,
action: 'VIEW_DASHBOARD',
timestamp: new Date().toISOString(),
})
// Fetch patient data securely
const { data: patientData } = await supabase
.from('patients')
.select('*')
.eq('user_id', user.id)
.single()
return (
<DashboardLayout patient={patientData} />
)
}
Dashboard components to build:
- Patient profile management with edit capabilities
- Upcoming and past appointments view
- Secure messaging interface
- Notification preferences and settings
Phase 4: AI Symptom Checker
This is the core AI feature. Design a conversational flow that helps users describe symptoms while being clear about limitations.
// app/api/symptom-check/route.ts
import Anthropic from '@anthropic-ai/sdk'
import { NextRequest, NextResponse } from 'next/server'
import { rateLimit } from '@/lib/rate-limit'
import { validateSession } from '@/lib/auth'
import { logSymptomQuery } from '@/lib/audit'
const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
})
const SYSTEM_PROMPT = `You are a helpful health information assistant.
Your role is to help users understand their symptoms and provide general
health information.
IMPORTANT GUIDELINES:
- Always remind users this is NOT medical advice
- Never diagnose conditions
- Recommend consulting healthcare providers for proper evaluation
- Ask clarifying questions about symptom duration, severity, and context
- Flag emergency symptoms that require immediate medical attention
- Be empathetic but factual
If symptoms suggest an emergency (chest pain, difficulty breathing,
severe bleeding, etc.), immediately advise calling emergency services.`
export async function POST(req: NextRequest) {
// Rate limiting
const rateLimitResult = await rateLimit(req)
if (!rateLimitResult.success) {
return NextResponse.json(
{ error: 'Too many requests. Please wait before trying again.' },
{ status: 429 }
)
}
// Validate session
const session = await validateSession(req)
if (!session) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
)
}
const { message, conversationHistory } = await req.json()
// Log query for audit (without storing actual symptoms for privacy)
await logSymptomQuery({
userId: session.user.id,
timestamp: new Date().toISOString(),
})
const response = await anthropic.messages.create({
model: 'claude-3-sonnet-20240229',
max_tokens: 1024,
system: SYSTEM_PROMPT,
messages: [
...conversationHistory,
{ role: 'user', content: message }
],
})
return NextResponse.json({
response: response.content[0].text,
disclaimer: 'This information is for educational purposes only and is not medical advice.',
})
}
Symptom checker features:
- Conversational UI with message history
- Clear medical disclaimers on every response
- Emergency symptom detection and alerts
- Symptom history tracking for user reference
- Triage recommendations (urgent, soon, routine)
Phase 5: Appointment System
Integrate scheduling functionality with provider availability management.
// lib/scheduling.ts - Appointment management
import { createServerClient } from '@/lib/supabase-server'
export interface Appointment {
id: string
patientId: string
providerId: string
dateTime: string
type: 'in-person' | 'telehealth'
status: 'scheduled' | 'completed' | 'cancelled'
notes?: string
}
export async function bookAppointment(
patientId: string,
providerId: string,
dateTime: string,
type: Appointment['type']
): Promise<Appointment> {
const supabase = createServerClient()
// Check provider availability
const { data: existing } = await supabase
.from('appointments')
.select('*')
.eq('provider_id', providerId)
.eq('date_time', dateTime)
.eq('status', 'scheduled')
if (existing && existing.length > 0) {
throw new Error('Time slot not available')
}
const { data, error } = await supabase
.from('appointments')
.insert({
patient_id: patientId,
provider_id: providerId,
date_time: dateTime,
type,
status: 'scheduled',
})
.select()
.single()
if (error) throw error
return data
}
Phase 6: Security and Compliance
Implement comprehensive security measures and audit capabilities.
// lib/encryption.ts - Data encryption utilities
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto'
const ALGORITHM = 'aes-256-gcm'
const KEY = Buffer.from(process.env.ENCRYPTION_KEY!, 'hex')
export function encryptData(data: string): string {
const iv = randomBytes(16)
const cipher = createCipheriv(ALGORITHM, KEY, iv)
let encrypted = cipher.update(data, 'utf8', 'hex')
encrypted += cipher.final('hex')
const authTag = cipher.getAuthTag()
return iv.toString('hex') + ':' + authTag.toString('hex') + ':' + encrypted
}
export function decryptData(encryptedData: string): string {
const [ivHex, authTagHex, encrypted] = encryptedData.split(':')
const iv = Buffer.from(ivHex, 'hex')
const authTag = Buffer.from(authTagHex, 'hex')
const decipher = createDecipheriv(ALGORITHM, KEY, iv)
decipher.setAuthTag(authTag)
let decrypted = decipher.update(encrypted, 'hex', 'utf8')
decrypted += decipher.final('utf8')
return decrypted
}
// lib/audit.ts - Audit logging middleware
import { createServerClient } from '@/lib/supabase-server'
export interface AuditLog {
userId: string
action: string
resource?: string
resourceId?: string
ipAddress?: string
userAgent?: string
timestamp: string
metadata?: Record<string, any>
}
export async function logDataAccess(log: AuditLog): Promise<void> {
const supabase = createServerClient()
await supabase.from('audit_logs').insert({
user_id: log.userId,
action: log.action,
resource: log.resource,
resource_id: log.resourceId,
ip_address: log.ipAddress,
user_agent: log.userAgent,
timestamp: log.timestamp,
metadata: log.metadata,
})
}
// Middleware for automatic audit logging
export function withAuditLog(
handler: Function,
action: string
) {
return async (req: Request, ...args: any[]) => {
const session = await getSession(req)
if (session?.user) {
await logDataAccess({
userId: session.user.id,
action,
ipAddress: req.headers.get('x-forwarded-for') || 'unknown',
userAgent: req.headers.get('user-agent') || 'unknown',
timestamp: new Date().toISOString(),
})
}
return handler(req, ...args)
}
}
Important Disclaimers
The AI symptom checker provides preliminary guidance only. It is NOT a substitute for professional medical advice, diagnosis, or treatment. Users must always consult qualified healthcare providers for medical decisions.
Make sure your application clearly communicates these important points to users:
- AI provides preliminary guidance only - The symptom checker helps users articulate their symptoms and provides general health information, but cannot diagnose conditions
- Not a replacement for medical professionals - Always recommend that users consult with healthcare providers for proper evaluation and treatment
- Users should always consult healthcare providers - Build in prominent disclaimers and require acknowledgment before using the symptom checker
- Data handling considerations - Be transparent about how health information is stored, processed, and protected
This guide provides technical implementation guidance. For production healthcare applications, consult with legal and compliance experts regarding HIPAA, GDPR, and other applicable regulations in your jurisdiction.
Additional Code Examples
Secure Supabase Client Setup
// lib/supabase-server.ts
import { createServerClient as createClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export function createServerClient() {
const cookieStore = cookies()
return createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value
},
set(name: string, value: string, options: any) {
cookieStore.set({ name, value, ...options })
},
remove(name: string, options: any) {
cookieStore.set({ name, value: '', ...options })
},
},
}
)
}
Encrypted Data Storage Pattern
// lib/patient-notes.ts - Encrypted patient notes
import { encryptData, decryptData } from './encryption'
import { createServerClient } from './supabase-server'
import { logDataAccess } from './audit'
export async function savePatientNote(
patientId: string,
note: string,
userId: string
): Promise<void> {
const supabase = createServerClient()
// Encrypt sensitive note content
const encryptedNote = encryptData(note)
await supabase.from('patient_notes').insert({
patient_id: patientId,
encrypted_content: encryptedNote,
created_by: userId,
created_at: new Date().toISOString(),
})
// Log the write operation
await logDataAccess({
userId,
action: 'CREATE_PATIENT_NOTE',
resource: 'patient_notes',
resourceId: patientId,
timestamp: new Date().toISOString(),
})
}
export async function getPatientNotes(
patientId: string,
userId: string
): Promise<string[]> {
const supabase = createServerClient()
const { data } = await supabase
.from('patient_notes')
.select('encrypted_content')
.eq('patient_id', patientId)
.order('created_at', { ascending: false })
// Log the read operation
await logDataAccess({
userId,
action: 'READ_PATIENT_NOTES',
resource: 'patient_notes',
resourceId: patientId,
timestamp: new Date().toISOString(),
})
// Decrypt notes for display
return (data || []).map(row => decryptData(row.encrypted_content))
}
Common Issues and Solutions
Here are some common issues you might encounter and how to solve them:
Healthcare apps should have shorter session timeouts. Implement a warning modal that appears before session expiry, and ensure users are redirected to login on timeout. Store session state in memory rather than localStorage for sensitive data.
Rate Limiting Symptom Queries
Implement rate limiting to prevent abuse of the AI symptom checker. A reasonable limit is 10-20 queries per hour per user. Use Redis or in-memory storage for rate limit tracking:
// lib/rate-limit.ts
const rateLimitMap = new Map<string, { count: number; resetTime: number }>()
export async function rateLimit(
req: Request,
limit = 20,
windowMs = 3600000 // 1 hour
): Promise<{ success: boolean; remaining: number }> {
const ip = req.headers.get('x-forwarded-for') || 'unknown'
const now = Date.now()
const record = rateLimitMap.get(ip)
if (!record || now > record.resetTime) {
rateLimitMap.set(ip, { count: 1, resetTime: now + windowMs })
return { success: true, remaining: limit - 1 }
}
if (record.count >= limit) {
return { success: false, remaining: 0 }
}
record.count++
return { success: true, remaining: limit - record.count }
}
Handling Sensitive Data in Logs
Never log actual symptom descriptions or personal health information. Log only metadata such as timestamps, user IDs (hashed if needed), and action types. Implement log redaction for any accidental PII exposure.
MFA Recovery Flows
Provide secure account recovery options for users who lose access to their MFA device. This typically involves email verification combined with security questions, followed by a mandatory MFA re-enrollment.
Next Steps
Once you have completed this guide, consider these enhancements to make your patient portal even more robust:
- Add telehealth video integration - Integrate WebRTC or a service like Twilio for secure video consultations
- Implement prescription management - Add medication tracking, refill reminders, and pharmacy integration
- Build provider dashboard - Create the healthcare provider side of the portal for managing patients and appointments
- Add health metric tracking - Integrate with wearables and allow manual entry of vitals like blood pressure, weight, and glucose levels
- Implement insurance verification - Add real-time insurance eligibility checking before appointments
Follow the Vibe Coding Enthusiast
Follow JD — product updates on LinkedIn, personal takes on X.