Patient API Documentation

PATIENT_API.md

Patient API Documentation

Base path: /api/patients Last updated: 2026-04-04

This document covers every patient-facing API endpoint — authentication, self-service portal data, and public registration. Staff-only CRUD endpoints (e.g. GET /api/patients, DELETE /api/patients/[id]) are documented in the Staff API docs.


Table of Contents

  1. Authentication Overview
  2. Rate Limiting
  3. Authentication Endpoints
  4. Public Endpoints
  5. Patient Portal — Self-Service
  6. Patient Appointments (Portal)
  7. Response Format
  8. Error Reference
  9. Data Models

1. Authentication Overview

All patient portal routes require a valid patient_session cookie. This cookie is an HS256 JWT signed with SESSION_SECRET, set as HttpOnly, SameSite=Lax, valid for 7 days.

Session payload structure

{
  "patientId": "664abc...",
  "patientCode": "CLINIC-0001",
  "type": "patient",
  "email": "jane@example.com",
  "iat": 1712000000,
  "exp": 1712604800
}

How to authenticate (login flow)

Choose one:
  ┌──────────────────┐    ┌─────────────────────────────┐    ┌──────────────────────────────┐
  │  QR code scan    │    │  Email + password            │    │  Phone OTP                   │
  │  POST /qr-login  │    │  POST /auth/login            │    │  POST /auth/otp/request      │
  └────────┬─────────┘    └─────────────┬───────────────┘    │  POST /auth/otp/verify       │
           │                            │                     └──────────────┬───────────────┘
           └────────────────────────────┴──────────────────────────────────┘
                                        │
                              patient_session cookie set
                                        │
                              Access /api/patients/me/*

2. Rate Limiting

Authentication endpoints use strict rate limiting (5 requests per 15-minute window per IP). Exceeding the limit returns HTTP 429 with a Retry-After header.

Endpoint groupLimiterWindowMax requests
Auth (/auth/*, /qr-login)auth15 min5
Public registrationpublic1 min20
Portal data (/me/*, /appointments)None (session-gated)

429 response:

{
  "success": false,
  "error": "Too many authentication attempts, please try again later",
  "retryAfter": 843
}

Headers: Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset


3. Authentication Endpoints

3.1 POST /api/patients/auth/login

Email + password login. Issues a patient_session cookie on success.

Auth required: No
Rate limited: Yes (auth limiter)

Request body:

FieldTypeRequiredDescription
emailstringYesPatient's registered email address
passwordstringYesPlain-text password (bcrypt-compared)
tenantIdstringNoMongoDB ObjectId of the clinic. Scopes the lookup to a specific tenant when a patient is registered at multiple clinics.
{
  "email": "jane.doe@example.com",
  "password": "MySecurePass123",
  "tenantId": "664abc123def456789000001"
}

Success response — 200:

{
  "success": true,
  "message": "Login successful",
  "data": {
    "patientId": "664abc...",
    "patientCode": "CLINIC-0001",
    "firstName": "Jane",
    "lastName": "Doe",
    "email": "jane.doe@example.com"
  }
}

Cookie set: patient_session (HttpOnly, 7 days)

Error responses:

StatusConditionError message
400Missing email or password"Email and password are required"
401Wrong credentials"Invalid email or password"
401Account has no password set"This account does not have a password set...", code: "NO_PASSWORD"
403Account is inactive"Patient account is inactive. Please contact the clinic."
429Rate limit exceeded"Too many authentication attempts..."

Note: When code: "NO_PASSWORD" is returned, redirect the patient to QR or OTP login. Use POST /api/patients/me/change-password to set an initial password after any successful login.


3.2 POST /api/patients/auth/otp/request

Generates a 6-digit OTP, hashes it, stores it with a 5-minute expiry, and sends it to the patient's registered phone via SMS (Twilio).

Auth required: No
Rate limited: Yes (auth limiter)
Security: Always returns the same success message regardless of whether the phone number exists (prevents enumeration).

Request body:

FieldTypeRequiredDescription
phonestringYesPhone number in any format. Auto-normalized to E.164.
tenantIdstringNoScope lookup to a specific clinic.
{
  "phone": "+63 917 123 4567",
  "tenantId": "664abc123def456789000001"
}

Success response — 200 (always returned, even if phone not found):

{
  "success": true,
  "message": "If a matching account is found, an OTP will be sent to your phone within 5 minutes."
}

OTP behavior:

  • OTP is 6 digits, zero-padded
  • Stored as bcrypt hash in patient.otp (select: false)
  • Expiry: 5 minutes (patient.otpExpiry)
  • Attempt counter reset to 0 on each new request
  • Searches both patient.phone and patient.contacts.phone

3.3 POST /api/patients/auth/otp/verify

Verifies the OTP and issues a patient_session cookie.

Auth required: No
Rate limited: Yes (auth limiter)

Request body:

FieldTypeRequiredDescription
phonestringYesSame number used in /otp/request
otpstringYes6-digit code received via SMS
tenantIdstringNoSame tenant used in /otp/request
{
  "phone": "+63 917 123 4567",
  "otp": "482916",
  "tenantId": "664abc123def456789000001"
}

Success response — 200:

{
  "success": true,
  "message": "Login successful",
  "data": {
    "patientId": "664abc...",
    "patientCode": "CLINIC-0001",
    "firstName": "Jane",
    "lastName": "Doe",
    "email": "jane.doe@example.com"
  }
}

Cookie set: patient_session (HttpOnly, 7 days)

Error responses:

StatusConditionError message
400Missing phone or otp"Phone number and OTP are required"
401Wrong OTP or no OTP found"Invalid or expired OTP"
401OTP expired"OTP has expired. Please request a new one."
4295+ wrong attempts"Too many incorrect attempts. Please request a new OTP."

Attempt logic:

  • Each wrong OTP increments patient.otpAttempts
  • At 5 failed attempts, OTP is invalidated and patient must request a new one
  • On success, otp, otpExpiry, and otpAttempts are cleared from the document

3.4 POST /api/patients/qr-login

Authenticates a patient by scanning their clinic-issued QR code.

Auth required: No
Rate limited: Yes (auth limiter)

Request body:

FieldTypeRequiredDescription
qrCodestring or objectYesJSON string or parsed object from the QR code
tenantIdstringNoOverride tenant from body (falls back to QR data or subdomain)

Expected QR code payload (JSON-encoded string):

{
  "type": "patient_login",
  "patientId": "664abc...",
  "patientCode": "CLINIC-0001",
  "tenantId": "664abc123def456789000001"
}

Success response — 200:

{
  "success": true,
  "message": "Login successful",
  "data": {
    "patientId": "664abc...",
    "patientCode": "CLINIC-0001",
    "firstName": "Jane",
    "lastName": "Doe",
    "email": "jane.doe@example.com"
  }
}

Error responses:

StatusConditionError message
400No QR code provided"QR code is required"
400Malformed JSON"Invalid QR code format"
400Missing patient identifier"Patient identification not found in QR code"
400Wrong QR type"Invalid QR code type. This QR code is not for patient login."
403Inactive patient"Patient account is inactive. Please contact the clinic."
404Patient not found"Patient not found"

3.5 GET /api/patients/session

Returns the authenticated patient's profile and optionally their related clinical data in a single request.

Auth required: Yes (patient_session cookie)

Query parameters:

ParamTypeDescription
includestringComma-separated list of data to include. Accepted values: appointments, visits, prescriptions, labResults, invoices, documents, referrals, all

Example:

GET /api/patients/session?include=appointments,prescriptions
GET /api/patients/session?include=all

Success response — 200:

{
  "success": true,
  "data": {
    "patient": {
      "_id": "664abc...",
      "patientCode": "CLINIC-0001",
      "firstName": "Jane",
      "lastName": "Doe",
      "dateOfBirth": "1990-05-15T00:00:00.000Z",
      "sex": "female",
      "email": "jane.doe@example.com",
      "phone": "+63 917 123 4567",
      "address": { "street": "123 Main St", "city": "Manila", "state": "NCR", "zipCode": "1000" },
      "emergencyContact": { "name": "John Doe", "phone": "+63 917 987 6543", "relationship": "Spouse" },
      "allergies": [],
      "medicalHistory": "",
      "preExistingConditions": [],
      "discountEligibility": {}
    },
    "appointments": [ /* up to 10, sorted by appointmentDate desc */ ],
    "visits": [ /* up to 10, sorted by date desc */ ],
    "prescriptions": [ /* up to 10, sorted by issuedAt desc */ ],
    "labResults": [ /* up to 10, sorted by orderDate desc */ ],
    "invoices": [ /* up to 10, sorted by createdAt desc */ ],
    "documents": [ /* up to 20, sorted by uploadDate desc — non-confidential only */ ],
    "referrals": [ /* up to 10, sorted by referredDate desc */ ]
  }
}

Tip: For paginated data, use the dedicated /me/* endpoints instead.


3.6 DELETE /api/patients/session

Logs the patient out by clearing the patient_session cookie.

Auth required: No (works even with invalid/expired session)

Success response — 200:

{
  "success": true,
  "message": "Logged out successfully"
}

4. Public Endpoints

4.1 POST /api/patients/public

Patient self-registration. No authentication required. Creates a new patient record.

Auth required: No
Rate limited: Yes (public limiter — 20 req/min)

Request body:

FieldTypeRequiredDescription
firstNamestringYes
lastNamestringYes
phonestringYes
dateOfBirthstringYesISO 8601 date
sexstringYes"male", "female", or "other"
emailstringNoIf omitted, a placeholder email is auto-generated
addressobjectYes{ street, city, state, zipCode }
tenantIdstringNoAssociates patient with a clinic
middleNamestringNo
suffixstringNo
emergencyContactobjectNo{ name, phone, relationship }

Success response — 201:

{
  "success": true,
  "message": "Patient registration successful. Your patient code is: CLINIC-0042",
  "data": { /* full patient document */ }
}

Error responses:

StatusCondition
400Missing required fields
400Validation error (e.g. invalid email format, invalid sex value)
409Duplicate email in same tenant
503Database connection error

5. Patient Portal — Self-Service

All routes in this section require the patient_session cookie. All return 401 if unauthenticated, 403 if the account is inactive.


5.1 GET /api/patients/me

Returns the complete patient profile of the currently authenticated patient.

Auth required: Yes

Success response — 200:

{
  "success": true,
  "data": {
    "_id": "664abc...",
    "patientCode": "CLINIC-0001",
    "tenantIds": ["664abc123def456789000001"],
    "firstName": "Jane",
    "middleName": "Marie",
    "lastName": "Doe",
    "suffix": null,
    "dateOfBirth": "1990-05-15T00:00:00.000Z",
    "sex": "female",
    "civilStatus": "married",
    "nationality": "Filipino",
    "occupation": "Engineer",
    "email": "jane.doe@example.com",
    "phone": "+63 917 123 4567",
    "address": {
      "street": "123 Main St",
      "city": "Manila",
      "state": "NCR",
      "zipCode": "1000"
    },
    "emergencyContact": {
      "name": "John Doe",
      "phone": "+63 917 987 6543",
      "relationship": "Spouse"
    },
    "identifiers": {
      "philHealth": "12-345678901-2",
      "govId": "A1234567"
    },
    "medicalHistory": "No significant history.",
    "preExistingConditions": [],
    "allergies": [],
    "discountEligibility": {},
    "active": true,
    "createdAt": "2024-01-10T08:00:00.000Z",
    "updatedAt": "2024-04-01T10:30:00.000Z"
  }
}

password, otp, otpExpiry, otpAttempts are never returned (schema select: false).


5.2 PATCH /api/patients/me

Updates the patient's own profile. Only safe fields are accepted; system and auth fields are silently stripped.

Auth required: Yes

Blocked fields (silently ignored if sent): patientCode, tenantIds, attachments, password, otp, otpExpiry, otpAttempts, active, _id, __v, createdAt, updatedAt

Updatable fields include: firstName, middleName, lastName, suffix, dateOfBirth, sex, civilStatus, nationality, occupation, email, phone, contacts, address, emergencyContact, identifiers, medicalHistory, allergies, socialHistory, familyHistory

Request body (partial update — send only fields to change):

{
  "phone": "+63 917 000 1111",
  "address": {
    "street": "456 New Ave",
    "city": "Quezon City",
    "state": "NCR",
    "zipCode": "1100"
  },
  "emergencyContact": {
    "name": "Mary Doe",
    "phone": "+63 917 555 6666",
    "relationship": "Sister"
  }
}

Success response — 200:

{
  "success": true,
  "message": "Profile updated successfully",
  "data": { /* full updated patient document */ }
}

Error responses:

StatusCondition
400All provided fields were blocked
400Mongoose validation error (e.g. invalid email format)
404Patient not found

5.3 POST /api/patients/me/change-password

Sets or changes the patient's password.

Auth required: Yes

Request body:

FieldTypeRequiredDescription
newPasswordstringYesMin 8 characters
currentPasswordstringConditionalRequired only if a password already exists on the account
{
  "currentPassword": "OldPass123",
  "newPassword": "NewSecurePass456"
}

Success response — 200:

{
  "success": true,
  "message": "Password updated successfully"
}

Error responses:

StatusConditionError
400newPassword missing or < 8 chars"New password must be at least 8 characters long"
400Account has password but currentPassword not provided"Current password is required"
401currentPassword is wrong"Current password is incorrect"

First-time password setup: If the patient logs in via QR or OTP and has no password, calling this endpoint with only newPassword (no currentPassword) will set their password for the first time.


5.4 GET /api/patients/me/visits

Returns a paginated list of the patient's visit history.

Auth required: Yes

Query parameters:

ParamTypeDefaultDescription
pagenumber1Page number
limitnumber10Results per page (max 50)
tenantIdstringFilter to a specific clinic (multi-clinic patients)

Success response — 200:

{
  "success": true,
  "data": [
    {
      "_id": "664abc...",
      "date": "2024-03-15T09:00:00.000Z",
      "type": "consultation",
      "chiefComplaint": "Headache",
      "diagnosis": "Tension headache",
      "provider": { "_id": "...", "firstName": "Dr. Juan", "lastName": "Cruz", "email": "dr.cruz@clinic.com" },
      "tenantId": "664abc123def456789000001"
    }
  ],
  "pagination": {
    "total": 24,
    "page": 1,
    "limit": 10,
    "totalPages": 3
  }
}

5.5 GET /api/patients/me/visits/[id]

Returns the full detail of a single visit. Ownership is enforced — only visits belonging to the authenticated patient are returned.

Auth required: Yes

URL params: id — MongoDB ObjectId of the visit

Success response — 200:

{
  "success": true,
  "data": { /* full visit document with provider populated */ }
}

Error responses:

StatusCondition
404Visit not found or does not belong to this patient

5.6 GET /api/patients/me/prescriptions

Returns a paginated list of the patient's prescriptions.

Auth required: Yes

Query parameters: page, limit (max 50), tenantId

Success response — 200:

{
  "success": true,
  "data": [
    {
      "_id": "664abc...",
      "issuedAt": "2024-03-15T09:00:00.000Z",
      "status": "active",
      "medications": [
        { "name": "Amoxicillin", "dosage": "500mg", "frequency": "3x daily", "duration": "7 days" }
      ],
      "prescribedBy": { "firstName": "Dr. Juan", "lastName": "Cruz", "email": "..." }
    }
  ],
  "pagination": { "total": 5, "page": 1, "limit": 10, "totalPages": 1 }
}

5.7 GET /api/patients/me/lab-results

Returns a paginated list of the patient's lab results.

Auth required: Yes

Query parameters: page, limit (max 50), tenantId

Success response — 200:

{
  "success": true,
  "data": [
    {
      "_id": "664abc...",
      "testName": "Complete Blood Count",
      "status": "available",
      "orderDate": "2024-03-14T00:00:00.000Z",
      "results": { /* structured result data */ }
    }
  ],
  "pagination": { "total": 8, "page": 1, "limit": 10, "totalPages": 1 }
}

5.8 GET /api/patients/me/invoices

Returns a paginated list of the patient's invoices plus an outstanding balance summary.

Auth required: Yes

Query parameters:

ParamTypeDefaultDescription
pagenumber1
limitnumber10Max 50
statusstringFilter by status: paid, unpaid, partial, cancelled
tenantIdstringFilter to a specific clinic

Success response — 200:

{
  "success": true,
  "data": [
    {
      "_id": "664abc...",
      "invoiceNumber": "INV-000042",
      "total": 2500.00,
      "status": "partial",
      "payments": [{ "amount": 1000, "method": "cash", "date": "..." }],
      "createdAt": "2024-03-15T09:00:00.000Z"
    }
  ],
  "summary": {
    "outstandingBalance": 1500.00,
    "unpaidCount": 2
  },
  "pagination": { "total": 12, "page": 1, "limit": 10, "totalPages": 2 }
}

5.9 GET /api/patients/me/documents

Returns a paginated list of the patient's documents. Confidential documents are always excluded.

Auth required: Yes

Query parameters:

ParamTypeDefaultDescription
pagenumber1
limitnumber20Max 50
categorystringFilter by document category
tenantIdstringFilter to a specific clinic

Success response — 200:

{
  "success": true,
  "data": [
    {
      "_id": "664abc...",
      "documentCode": "DOC-0001",
      "title": "Blood Test Results March 2024",
      "description": "CBC panel results",
      "category": "lab",
      "documentType": "pdf",
      "filename": "cbc-march-2024.pdf",
      "size": 204800,
      "uploadDate": "2024-03-16T08:00:00.000Z"
    }
  ],
  "pagination": { "total": 6, "page": 1, "limit": 20, "totalPages": 1 }
}

To download a document, use the staff-accessible /api/documents/[id]/download endpoint (requires staff authentication).


5.10 GET /api/patients/me/notifications

Returns a patient-centric notification feed derived from recent clinical activity (appointments, lab results, invoices, prescriptions) from the last 90 days.

Auth required: Yes

Query parameters:

ParamTypeDefaultDescription
pagenumber1
limitnumber20Max 50
unreadOnlybooleanfalseReturn only unread notifications

Success response — 200:

{
  "success": true,
  "unreadCount": 3,
  "data": [
    {
      "id": "apt-664abc...",
      "type": "appointment",
      "title": "Appointment Confirmed",
      "message": "Your appointment on Mon, Apr 7 at 09:00 has been confirmed.",
      "date": "2024-04-05T14:30:00.000Z",
      "read": false,
      "actionUrl": "/patient/portal?tab=appointments",
      "metadata": { "appointmentCode": "APT-000042", "status": "confirmed" }
    },
    {
      "id": "lab-664abc...",
      "type": "lab_result",
      "title": "Lab Result Requires Attention",
      "message": "Your CBC result is abnormal. Please contact your doctor.",
      "date": "2024-04-04T11:00:00.000Z",
      "read": false,
      "actionUrl": "/patient/portal?tab=lab-results",
      "metadata": { "testName": "Complete Blood Count", "status": "abnormal" }
    },
    {
      "id": "inv-664abc...",
      "type": "invoice",
      "title": "Outstanding Balance",
      "message": "Invoice #INV-000042 has an outstanding balance of 1500.",
      "date": "2024-04-01T09:00:00.000Z",
      "read": true,
      "actionUrl": "/patient/portal?tab=invoices",
      "metadata": { "invoiceNumber": "INV-000042", "total": 2500, "status": "partial" }
    }
  ],
  "pagination": { "total": 7, "page": 1, "limit": 20, "totalPages": 1 }
}

Notification ID format:

PrefixSource
apt-{id}Appointment
lab-{id}Lab result
inv-{id}Invoice
rx-{id}Prescription

Notification types and triggers:

TypeTrigger conditions
appointmentStatus is confirmed, cancelled, pending, or scheduled; updated in last 90 days
lab_resultStatus is available, abnormal, or critical; updated in last 90 days
invoiceStatus is unpaid or partial (no time restriction — always shown until resolved)
prescriptionCreated in last 90 days

5.11 PATCH /api/patients/me/notifications

Marks one or more notifications as read.

Auth required: Yes

Option A — Mark specific notifications:

{
  "ids": ["apt-664abc...", "lab-664abc..."]
}

Option B — Mark all as read:

{
  "markAllRead": true
}

Success response — 200:

{
  "success": true,
  "message": "2 notification(s) marked as read"
}

or

{
  "success": true,
  "message": "All notifications marked as read"
}

Error responses:

StatusCondition
400Neither ids array nor markAllRead: true provided
400ids is empty

How read-state works: IDs are stored in patient.readNotificationIds (a string array on the Patient document). The GET endpoint cross-references this list. markAllRead clears the list, so the next GET will correctly recalculate unread state from freshly derived notifications.


6. Patient Appointments (Portal)

6.1 GET /api/patients/appointments

Returns available doctors and optionally available time slots for a given date and doctor.

Auth required: Yes (patient_session cookie)

Query parameters:

ParamTypeDescription
datestringISO 8601 date. If provided with doctorId, returns available slots.
doctorIdstringMongoDB ObjectId of the doctor

Example:

GET /api/patients/appointments
GET /api/patients/appointments?date=2024-04-10&doctorId=664abc...

Success response (no date/doctorId) — 200:

{
  "success": true,
  "data": {
    "doctors": [
      { "_id": "664abc...", "firstName": "Juan", "lastName": "Cruz", "specialization": "General Medicine", "schedule": {} }
    ]
  }
}

Success response (with date + doctorId) — 200:

{
  "success": true,
  "data": {
    "doctors": [ /* same as above */ ],
    "availableSlots": ["09:00", "09:30", "10:00", "11:30", "14:00"]
  }
}

Time slots are 30-minute intervals between 09:00 and 17:00. Already-booked slots are excluded.


6.2 POST /api/patients/appointments

Books an appointment for the authenticated patient.

Auth required: Yes

Request body:

FieldTypeRequiredDescription
doctorIdstringYesMongoDB ObjectId
appointmentDatestringYesISO 8601 date
appointmentTimestringYesFormat: HH:mm (24-hour)
reasonstringNoMax 500 characters
{
  "doctorId": "664abc...",
  "appointmentDate": "2024-04-10",
  "appointmentTime": "10:30",
  "reason": "Annual check-up"
}

Success response — 201:

{
  "success": true,
  "message": "Appointment request submitted successfully. You will receive a confirmation shortly.",
  "data": { /* full appointment document with patient and doctor populated */ }
}

Error responses:

StatusCondition
400Missing required fields
400Invalid date or time format
404Doctor not found in patient's tenant
409Time slot already taken by another patient
409Patient already has an appointment at this time

Appointments are created with status: "pending" and must be confirmed by clinic staff. An SMS confirmation is sent to the patient's phone.


6.3 DELETE /api/patients/appointments/[id]

Cancels an appointment belonging to the authenticated patient.

Auth required: Yes

URL params: id — MongoDB ObjectId of the appointment

Constraints:

  • Only the patient who booked the appointment can cancel it
  • Cannot cancel appointments that are already completed or cancelled
  • Cannot cancel past appointments

Success response — 200:

{
  "success": true,
  "message": "Appointment cancelled successfully"
}

Error responses:

StatusCondition
403Appointment does not belong to this patient
404Appointment not found
409Appointment cannot be cancelled (wrong status or past date)

7. Response Format

All responses follow this consistent shape:

Success:

{
  "success": true,
  "data": { /* payload */ },
  "message": "Optional human-readable message"
}

Paginated success:

{
  "success": true,
  "data": [ /* array of items */ ],
  "pagination": {
    "total": 100,
    "page": 2,
    "limit": 10,
    "totalPages": 10
  }
}

Error:

{
  "success": false,
  "error": "Human-readable error message"
}

8. Error Reference

HTTP StatusMeaning
400Bad request — validation error or missing field
401Unauthenticated — missing or invalid patient_session cookie
403Forbidden — account inactive, or resource not owned by patient
404Resource not found
409Conflict — duplicate record or scheduling conflict
429Rate limit exceeded
500Server error
503Database unavailable

9. Data Models

Patient Session JWT payload

{
  patientId: string;       // MongoDB ObjectId
  patientCode: string;     // e.g. "CLINIC-0001"
  type: "patient";         // always "patient"
  email: string;
  iat: number;             // issued at (UNIX)
  exp: number;             // expires at (UNIX, 7 days)
}

Patient document (relevant fields)

{
  _id: ObjectId;
  tenantIds: ObjectId[];         // clinics this patient belongs to
  patientCode: string;           // e.g. "CLINIC-0001"
  firstName: string;
  middleName?: string;
  lastName: string;
  suffix?: string;
  dateOfBirth: Date;
  sex: "male" | "female" | "other";
  civilStatus?: string;
  nationality?: string;
  occupation?: string;
  email?: string;
  phone: string;
  address: { street: string; city: string; state: string; zipCode: string };
  emergencyContact?: { name?: string; phone?: string; relationship?: string };
  identifiers?: { philHealth?: string; govId?: string };
  medicalHistory?: string;
  preExistingConditions?: Array<{ condition: string; status: "active"|"resolved"|"chronic" }>;
  allergies?: Array<string | { substance: string; reaction: string; severity: string }>;
  discountEligibility?: { pwd?: {...}; senior?: {...}; membership?: {...} };
  active?: boolean;
  // Auth fields — never returned in responses (select: false)
  password?: string;
  otp?: string;
  otpExpiry?: Date;
  otpAttempts?: number;
  // Notification read-state tracking
  readNotificationIds?: string[];
  createdAt: Date;
  updatedAt: Date;
}

Notification object

{
  id: string;                          // e.g. "apt-664abc...", "lab-664abc..."
  type: "appointment" | "lab_result" | "invoice" | "prescription";
  title: string;
  message: string;
  date: Date;
  read: boolean;
  actionUrl?: string;                  // e.g. "/patient/portal?tab=appointments"
  metadata?: Record<string, any>;     // type-specific fields (appointmentCode, status, etc.)
}

Generated from source code — app/api/patients/ — 2026-04-04