Advanced ~6 hours

Build an AI-Powered Patient Portal

Create a secure patient portal with AI symptom checking, appointment booking, medical record access, and provider communication - all with modern security practices.

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.

Pro Tip

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

Critical: AI Limitations

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
Compliance Note

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:

Session Timeout Handling

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