2. Data Model Documentation
This section is the developer's map of the platform's data. It is organized so you can start from a high-level architectural view and drill into any single domain. The authoritative typed contract is lib/schema.ts; where the production model adds structure beyond the mock schema (real keys, join tables, audit and consent), it is noted.
2.1 How to read this section
- 2.2 High-level architecture gives the whole picture: the domain map, the domain catalogue, and a condensed master ER diagram.
- 2.3 to 2.13 are the domains. Each has an ER diagram (with keys marked) and per-entity detail: purpose, key attributes, keys and indexes, constraints, and relationships.
- 2.14 to 2.16 cover cross-entity rules, the consolidated indexing strategy, and conventions.
Conventions
- Identifiers. Surrogate primary keys are UUIDs unless a natural, stable business key already exists. The
Caseis the exception: its primary key is the human-readablecaseId(the file number surfaced in the UI, formatGC-####). - Money. Stored as an integer minor unit plus an ISO-4217
currency. Never a float. All amounts on a case use the corridor currency. - Time. Timestamps are ISO-8601 UTC. Mutable entities carry
createdAtandupdatedAt; lifecycle entities also carry event timestamps (enteredAt,submittedAt,releasedAt). - Soft delete. Operational rows carry a nullable
deletedAt; hard deletion runs only on the retention schedule and is audited. A legal hold pauses deletion. - Tenancy. Every tenant-owned row carries a tenant key (
clinicId) and, for case data, thecaseId, enforced by row-level security. See multi-tenant architecture.
Key and constraint notation
The attribute tables and ER diagrams use this notation:
| Mark | Meaning |
|---|---|
| PK | Primary key |
| FK | Foreign key (references another entity) |
| UQ | Unique constraint |
| IDX | Indexed (non-unique) |
| NN | Not null (required) |
| CK | Check constraint (enumerated or ranged value) |
The case is the spine
Almost every operational record hangs off a Case, identified by caseId. A Case belongs to one Patient, runs along one Corridor (configured by a Country Pack), targets one Clinic, is owned by one Coordinator, and progresses through the 14 Stages. Keep this in mind: when in doubt about how two records relate, they usually relate through the Case.
2.2 High-level architecture
Domain map
The model divides into ten functional domains plus one cross-cutting domain. Arrows show the primary direction of reference (a domain reads or links to the domain it points at).
flowchart TD IAM[Identity and Access] --> CASE[Case Management] CORR[Corridors and Country Packs] --> CASE PROV[Provider and Marketplace] --> CASE CASE --> CLIN[Clinical and Records] CASE --> VISA[Visa and Immigration] CASE --> TRAVEL[Travel and Logistics] CASE --> PAY[Payments and Escrow] CASE --> COMMS[Communications] AGENT[Growth and Agents] --> CASE VISA --> CLIN PAY --> PROV CASE --> AUDIT[Audit, Consent and Lifecycle] CLIN --> AUDIT PAY --> AUDIT VISA --> AUDIT
Domain catalogue
| Domain | Section | Core entities | Kind |
|---|---|---|---|
| Identity & Access | 2.3 | User, Session, RoleGrant | Operational |
| Case Management | 2.4 | Case, Patient, Coordinator, Stage, StageProgress | Operational |
| Corridors & Country Packs | 2.5 | Corridor, CountryPack | Reference / config |
| Provider & Marketplace | 2.6 | Clinic, ClinicAccount, TeamMember, Specialty, Procedure, Review, Inquiry | Mixed |
| Clinical & Records | 2.7 | Document, Consultation, Opinion, CarePlan, Appointment | Operational |
| Visa & Immigration | 2.8 | VisaApplication, VisaChecklistItem, VisaTimelineStep | Operational |
| Travel & Logistics | 2.9 | TripPlan, FlightOption, StayOption, TransferOption, TravelSelection | Operational |
| Payments & Escrow | 2.10 | Quote, EscrowAccount, PaymentMilestone, LedgerEntry, Refund | Operational (regulated) |
| Communications | 2.11 | Thread, Message, Notification | Operational |
| Growth & Agents | 2.12 | Agent, Referral | Operational |
| Audit, Consent & Lifecycle | 2.13 | AuditEvent, Consent | Cross-cutting |
Master ER diagram (condensed)
The condensed view shows the backbone across domains. Each domain section below expands its own slice with full attributes.
erDiagram
USER ||--o{ CASE : "staffs / owns"
PATIENT ||--o{ CASE : "is subject of"
CORRIDOR ||--o{ CASE : "routes"
COUNTRY_PACK ||--|| CORRIDOR : "configures"
CLINIC ||--o{ CASE : "treats"
CLINIC ||--o{ CLINIC_USER : "employs"
CLINIC ||--o{ PROCEDURE_PRICE : "prices"
CLINIC ||--o{ INQUIRY : "receives"
COORDINATOR ||--o{ CASE : "owns"
CASE ||--o{ STAGE_PROGRESS : "advances through"
CASE ||--o{ DOCUMENT : "holds"
CASE ||--o{ CONSULTATION : "has"
CASE ||--o{ QUOTE : "receives"
CASE ||--|| VISA_APPLICATION : "has"
CASE ||--|| ESCROW_ACCOUNT : "funds"
CASE ||--o{ APPOINTMENT : "schedules"
CASE ||--o{ TRAVEL_SELECTION : "books"
CASE ||--o{ THREAD : "communicates via"
CASE ||--o{ REVIEW : "yields"
CASE ||--o{ CONSENT : "records"
CASE ||--o{ AUDIT_EVENT : "is audited by"
VISA_APPLICATION ||--o{ VISA_CHECKLIST_ITEM : "requires"
VISA_CHECKLIST_ITEM }o--o| DOCUMENT : "verified by"
ESCROW_ACCOUNT ||--o{ PAYMENT_MILESTONE : "schedules"
ESCROW_ACCOUNT ||--o{ LEDGER_ENTRY : "records"
THREAD ||--o{ MESSAGE : "contains"
AGENT ||--o{ REFERRAL : "submits"
REFERRAL }o--|| CASE : "attributes"2.3 Domain: Identity and Access
Authentication, sessions and role grants. In the reference build this is a client-side mock (lib/auth.tsx, two demo accounts); production replaces it with real identity. A User is the login principal; a Patient and a ClinicAccount each link to a User.
erDiagram
USER ||--o{ SESSION : "opens"
USER ||--o{ ROLE_GRANT : "is granted"
USER ||--o| PATIENT : "is"
USER ||--o{ CLINIC_USER : "is"
USER {
uuid id PK
string email UK "NN"
string passwordHash "NN"
boolean emailVerified
boolean mfaEnrolled
timestamp createdAt
timestamp deletedAt
}
SESSION {
uuid id PK
uuid userId FK "NN"
string refreshTokenHash
string ip
timestamp expiresAt
timestamp revokedAt
}
ROLE_GRANT {
uuid id PK
uuid userId FK "NN"
string role "CK"
string scopeType "CK: tenant|case|global"
string scopeId
}User
- Purpose. The login principal for every human (patient, clinic staff, internal staff, agent).
- Key attributes.
idPK;emailUQ NN;passwordHashNN;emailVerified,mfaEnrolled(staff and clinic users require MFA). - Indexes. Unique on
email. - Constraints. Email format validated; password policy enforced at the service layer; staff roles cannot authenticate without a completed MFA session.
- Relationships. One User opens many
Sessions, holds manyRoleGrants, and is linked one-to-one to aPatientor one-to-many toClinicUserrecords.
Session
- Purpose. A refresh-token-backed login session supporting rotation and revocation.
- Key attributes.
idPK;userIdFK NN;refreshTokenHash;expiresAt;revokedAt. - Indexes.
userIdIDX. - Constraints. Refresh-token reuse detection; idle and absolute lifetimes; revoked on logout.
RoleGrant
- Purpose. Binds a User to a role within a scope. This is the storage behind the RBAC matrix.
- Key attributes.
idPK;userIdFK NN;roleCK (patient, coordinator, specialist, provider, visa, travel, finance, admin, agent, medical_director, support);scopeTypeCK (tenant, case, global);scopeId(theclinicIdorcaseIdthe grant applies to). - Constraints. Deny by default; a grant is the only way a non-owner principal gains access to a tenant or case.
2.4 Domain: Case Management (the spine)
The operational core. Case is the root entity; Patient, Coordinator and the 14 Stages attach to it. StageProgress records movement through the lifecycle with SLA timers.
erDiagram
PATIENT ||--o{ CASE : "subject of"
COORDINATOR ||--o{ CASE : "owns"
CORRIDOR ||--o{ CASE : "routes"
CLINIC ||--o{ CASE : "treats"
CASE ||--o{ STAGE_PROGRESS : "advances"
STAGE ||--o{ STAGE_PROGRESS : "instantiated by"
CASE {
string caseId PK "format GC-####"
uuid patientId FK "NN"
string clinicId FK
string corridorId FK "NN"
uuid coordinatorId FK
int currentStage "CK 0..13"
string status "CK open|on_hold|closed|cancelled"
string dataRegime
string riskTier "CK standard|enhanced"
timestamp createdAt
timestamp updatedAt
}
PATIENT {
uuid id PK
string caseId FK "active case"
string name "NN"
int age "CK 0..120"
string origin
string treatment
string lang
int currentStage
}
COORDINATOR {
uuid id PK
string name
string langs
string status
}
STAGE {
string key PK
string label
string group "CK Discover|Plan|Treat|Return"
string owner
string slaTarget
}
STAGE_PROGRESS {
uuid id PK
string caseId FK "NN"
string stageKey FK "NN"
string status "CK done|active|upcoming"
uuid ownerUserId FK
timestamp enteredAt
timestamp dueAt
timestamp completedAt
}Case
- Purpose. The spine that ties every other domain together; one per treatment journey.
- Key attributes.
caseIdPK (human-readable file number);patientIdFK NN;clinicIdFK (chosen provider);corridorIdFK NN;coordinatorIdFK (named owner);currentStageCK (0 to 13);statusCK (open, on_hold, closed, cancelled);dataRegimeandriskTierdenormalized from the Country Pack so handling rules travel with the case. - Indexes.
patientId,clinicId,corridorId,coordinatorId,statusall IDX (the dominant access patterns are per-owner queues and per-corridor reporting). - Constraints.
currentStageadvances forward only; a regression requires an explicit, audited override. At most two attendants per case. - Relationships. Belongs to one Patient, Corridor, Clinic and Coordinator; parents Documents, Consultations, Quotes, the VisaApplication, the EscrowAccount, Appointments, TravelSelections, Threads, Reviews, Consents and AuditEvents.
Patient
- Purpose. The person seeking treatment (
Patientin the schema). - Key attributes.
idPK;caseIdFK (active case);nameNN;ageCK (0 to 120);origin,treatment,specialty,hospital,surgeon,surgeryDate,attendant,corridor,lang,currentStage. - Constraints.
name,originand all linked medical and identity records are sensitive PII (see retention and PII handling). - Relationships. Subject of one or more Cases; the active case is the one surfaced in the portal.
Coordinator
- Purpose. The named human who owns the patient relationship across all stages (
Coordinator). - Key attributes.
idPK;name;langs(languages spoken);reply(typical reply time);status. - Relationships. Owns many Cases; appears as a
Threadparticipant.
Stage and StageProgress
- Purpose.
Stageis reference data, the catalogue of the 14 lifecycle stages.StageProgressrecords a single case's movement through one stage with SLA timing. - Stage key attributes.
keyPK;label,short,groupCK (Discover, Plan, Treat, Return),owner(accountable role),slaTarget(service-level target). - StageProgress key attributes.
idPK;caseIdFK NN;stageKeyFK NN;statusCK (done, active, upcoming);ownerUserIdFK;enteredAt,dueAt(derived fromslaTarget),completedAt. - Indexes.
caseIdIDX; composite(caseId, status)for the active-stage lookup;dueAtIDX for SLA-breach queries. - Why it matters. The
ownerandslaTargetfields exist in the mock schema precisely so the planned operations console can drive SLA timers and escalation.
2.5 Domain: Corridors and Country Packs
A Corridor is a source-to-destination route. A CountryPack is its versioned, feature-flagged configuration: the rules engine behind visas, payments, language and risk.
erDiagram
CORRIDOR ||--|| COUNTRY_PACK : "current version"
COUNTRY_PACK ||--o{ COUNTRY_PACK : "supersedes (version history)"
CORRIDOR ||--o{ CASE : "routes"
CORRIDOR {
string id PK
string from "NN"
string to "NN"
string type "CK e-Visa|Paper visa"
string currency
string payment
string dataRegime
string riskTier "CK standard|enhanced"
boolean active
}
COUNTRY_PACK {
uuid id PK
string corridorId FK "NN"
int version "NN"
date effectiveFrom
json requiredDocuments
json featureFlags
string processingWindow
int feeMinor
}Corridor
- Purpose. Defines a route such as Oman to India and the high-level rules that apply to every case on it (
Corridor). - Key attributes.
idPK;from,toNN;typeCK (e-Visa, Paper visa);time(processing window);language,currency,payment,dataRegime;riskTierCK (standard, enhanced);active. - Indexes.
riskTierIDX (enhanced-diligence corridors). - Relationships. Routes many Cases; has one current Country Pack version.
CountryPack
- Purpose. The versioned configuration bundle for a corridor. Editing a corridor's rules means publishing a new Country Pack version, never mutating live cases.
- Key attributes.
idPK;corridorIdFK NN;versionNN;effectiveFrom;requiredDocuments(the visa document set),featureFlags,processingWindow,feeMinor, plus currency, payment,dataRegime,riskTier. - Constraints.
(corridorId, version)UQ; exactly one version is current per corridor at a time; versions are immutable once published. - Why it matters. Country Packs are the rules engine: the visa engine, payment currency, language assignment and risk tier all read from the corridor's current pack, so a corridor's behaviour changes without a code release.
2.6 Domain: Provider and Marketplace
The partner side: a Clinic (the marketplace and clinical record), the ClinicAccount and ClinicUser/ClinicTeamMember who operate it, the catalogue (Specialty, Procedure, ClinicProcedurePrice), reputation (Review, Reputation, Accreditation), and provider operations (ClinicInquiry, ClinicCase).
erDiagram
CLINIC ||--|| CLINIC_ACCOUNT : "operated via"
CLINIC ||--o{ CLINIC_TEAM_MEMBER : "staffs"
CLINIC ||--o{ CLINIC_PROCEDURE_PRICE : "prices"
CLINIC ||--o{ REVIEW : "receives"
CLINIC ||--o{ CLINIC_INQUIRY : "receives"
CLINIC ||--o{ CLINIC_CASE : "delivers"
SPECIALTY ||--o{ PROCEDURE : "categorises"
PROCEDURE ||--o{ CLINIC_PROCEDURE_PRICE : "priced as"
CLINIC {
string id PK
string name "NN"
string country "IDX"
json accreditations
json specialties
int priceFromUsd "IDX"
int savingsPct
boolean vetted "IDX"
}
CLINIC_ACCOUNT {
uuid id PK
string clinicId FK "UQ"
string contactEmail
string payoutAccount "masked"
string status "CK verified|pending|draft"
string joined
}
PROCEDURE {
string key PK
string name
string specialty FK
int priceFromUsd "IDX"
int refPriceUsd
int savingsPct
}
CLINIC_PROCEDURE_PRICE {
uuid id PK
string clinicId FK
string procedure FK
int priceUsd
string stay
}
REVIEW {
uuid id PK
string clinicId FK "IDX"
string caseId FK
int rating "CK 1..5"
string source "CK Trustpilot|Google|Verified visit"
boolean verified "IDX"
string date
}
CLINIC_INQUIRY {
uuid id PK
string clinicId FK "IDX"
string status "CK new|quoted|accepted|declined"
int budgetUsd
string date
}Clinic
- Purpose. A partner hospital or clinic: its public marketplace presence and its clinical identity (
Clinic). - Key attributes.
idPK;nameNN;city,country;accreditations[](JCI, NABH, ISO),specialties[],procedures[],reviews(aClinicReviewSummary);priceFromUsd,savingsPct,wait,founded,intlPatients,tags[];vetted. - Indexes.
countryIDX;priceFromUsdIDX;specialtiesandaccreditationsGIN (array containment for filters); partial index wherevetted = true; full-text search vector on name and blurb. - Constraints. A clinic is publicly listed only when its account
status = verified; only JCI or NABH-accredited clinics may be empanelled. - Relationships. Operated via one
ClinicAccount; staffsClinicTeamMembers; pricesProcedures; receivesReviews,ClinicInquirys; deliversClinicCases; treatsCases.
ClinicAccount and ClinicTeamMember
- ClinicAccount.
idPK;clinicIdFK UQ (links to the marketplaceClinic);contactName,contactEmail,phone;payoutAccount(masked);statusCK (verified, pending, draft);joined. This is the tenant root for clinic users. - ClinicTeamMember.
idPK;clinicIdFK;name,role,specialty,experience,highlight;statusCK (active, invited). The clinical staff and surgeons.
Specialty, Procedure, ClinicProcedurePrice
- Specialty.
keyPK;name,icon,blurb. Reference data categorising procedures. - Procedure.
keyPK;specialtyFK;name,priceFromUsdIDX,refPriceUsd,refCountry,savingsPct,stay,clinicCount,popular. The catalogue item. - ClinicProcedurePrice.
idPK;clinicIdFK;procedureFK;priceUsd,stay. Indicative price for one procedure at one clinic;(clinicId, procedure)UQ.
Review, Reputation, Accreditation
- Review.
idPK;clinicIdFK IDX;caseIdFK (a verified review ties to a completed, attributed case);ratingCK (1 to 5);sourceCK (Trustpilot, Google, Verified visit);verifiedIDX;author,country,date,procedure,title,body. Genuine negative verified reviews are not suppressed. - Reputation. Platform aggregate:
overall,totalReviews,verifiedPct,sources[](ReputationSource),distribution[]. Largely derived/materialized. - Accreditation.
codePK (JCI, NABH);name,body,blurb,facilities. Reference data.
ClinicInquiry and ClinicCase (provider operations)
- ClinicInquiry.
idPK;clinicIdFK IDX;patient,country,procedure,date;statusCK (new, quoted, accepted, declined);budgetUsd;message. Composite index(clinicId, status)powers the clinic inbox. - ClinicCase.
idPK;clinicIdFK;patient,procedure;stageCK (Inquiry, Quoted, Treatment planned, In treatment, Recovery, Closed);surgeryDate,valueUsd,coordinator. The provider's view of an active case; index(clinicId, stage)for the case board.
2.7 Domain: Clinical and Medical Records
The documents wallet, the specialist consultation and opinion, the care plan, and scheduling.
erDiagram
CASE ||--o{ DOCUMENT : "holds"
CASE ||--o{ CONSULTATION : "has"
CONSULTATION ||--o| OPINION : "produces"
CONSULTATION }o--o{ DOCUMENT : "reviews"
CASE ||--o{ CARE_PHASE : "plans"
CASE ||--o{ APPOINTMENT : "schedules"
DOCUMENT {
uuid id PK
string caseId FK "NN"
string name
string type "CK Identity|Medical|Visa|Financial|Travel"
string status "CK pending|verified"
string storageKey "encrypted"
uuid verifiedBy FK
timestamp createdAt
}
CONSULTATION {
uuid id PK
string caseId FK "NN"
string status "CK open|awaiting_records|opinion_ready|closed"
uuid specialistId FK
}
OPINION {
uuid id PK
uuid consultationId FK "UQ"
text body
uuid authorId FK
timestamp issuedAt
}
APPOINTMENT {
uuid id PK
string caseId FK "NN"
string kind "CK consult|admission|procedure|followup"
timestamp startsAt "IDX"
string status "CK scheduled|completed|cancelled|no_show"
}
CARE_PHASE {
uuid id PK
string caseId FK
string phase
json items
}Document
- Purpose. A single item in the patient's wallet (
Document), and the source of visa checklist completion. - Key attributes.
idPK;caseIdFK NN;name;typeCK (Identity, Medical, Visa, Financial, Travel);statusCK (pending, verified);size,date;storageKey(encrypted object-storage key, server-set);verifiedByFK (reviewer). - Indexes.
caseIdIDX; composite(caseId, type); partial index wherestatus = pending(the verification queue). - Constraints. File type and size validated at upload; virus-scanned before
verified; encrypted at rest; only an authorised reviewer can setverified. Verifying a document with a linked visa checklist item updates that item (the signature cross-screen flow).
Consultation and Opinion
- Consultation.
idPK;caseIdFK NN;statusCK (open, awaiting_records, opinion_ready, closed);specialistIdFK. Reviews a set of Documents (many-to-many). - Opinion.
idPK;consultationIdFK UQ (one opinion per consultation);body;authorIdFK;issuedAt. The opinion is the specialist's, recorded by the platform; authorship is stored explicitly.
Appointment
- Purpose. Scheduling for consultations, admission, procedure and follow-up tele-consults.
- Key attributes.
idPK;caseIdFK NN;kindCK (consult, admission, procedure, followup);startsAtIDX;statusCK (scheduled, completed, cancelled, no_show). - Constraints. Follow-up appointments default to day 7, 30 and 90; scheduling conflicts are rejected.
CarePhase and Vital
- CarePhase.
idPK;caseIdFK;phase(Pre-op, Procedure, Post-op, Recovery);items([label, status] tuples). Derived from the opinion; drives the Care Plan screen. - Vital. A
[label, value, unit]tuple shown on the care plan; display-oriented today.
2.8 Domain: Visa and Immigration
The flagship engine. A VisaApplication per case, driven by the corridor Country Pack, with a derived checklist and a status timeline.
erDiagram
CASE ||--|| VISA_APPLICATION : "has"
VISA_APPLICATION ||--o{ VISA_CHECKLIST_ITEM : "requires"
VISA_APPLICATION ||--o{ VISA_TIMELINE_STEP : "tracks"
VISA_CHECKLIST_ITEM }o--o| DOCUMENT : "completed by"
COUNTRY_PACK ||--o{ VISA_APPLICATION : "rules for"
VISA_APPLICATION {
uuid id PK
string caseId FK "UQ NN"
string type
string ref UK "govt reference"
string status "CK in_review|granted|rejected"
int feeMinor
timestamp submittedAt
timestamp grantedAt
}
VISA_CHECKLIST_ITEM {
uuid id PK
uuid visaApplicationId FK "NN"
string label
boolean done
uuid docId FK "nullable"
}
VISA_TIMELINE_STEP {
uuid id PK
uuid visaApplicationId FK "NN"
string label
boolean done
boolean active
string date
}VisaApplication
- Purpose. The case's medical-visa application and its live status (
Visa). - Key attributes.
idPK;caseIdFK UQ NN (one per case);type,country,from,fee,processing,validity;statusCK (in_review, granted, rejected);refUQ (government medical reference number);submittedAt,grantedAt. - Constraints. May be submitted only when every checklist item resolves to done (else
409); submission is idempotent. Required documents, fee, processing window and routing come from the corridor Country Pack.
VisaChecklistItem
- Purpose. One required item; the engine's unit of completeness (
VisaChecklistItem). - Key attributes.
idPK;visaApplicationIdFK NN;label;done;docIdFK (nullable link to aDocument). - Indexes.
visaApplicationIdIDX;docIdIDX (to resolve linked-document completion). - The signature rule. When
docIdis present,doneis derived from that document beingverified, not from a stored flag. This is what makes "upload a document" unlock "submit application".
VisaTimelineStep
idPK;visaApplicationIdFK NN;label,date,done,active(the single in-progress step). The vertical status stepper.
2.9 Domain: Travel and Logistics
The trip plan and the bookable options. The patient selects preferences; the Travel Desk confirms. Options are largely corridor-scoped reference data; TravelSelection is the per-case transactional record.
erDiagram
CASE ||--|| TRIP_PLAN : "has"
CASE ||--o{ TRAVEL_SELECTION : "chooses"
FLIGHT_OPTION ||--o{ TRAVEL_SELECTION : "selected in"
STAY_OPTION ||--o{ TRAVEL_SELECTION : "selected in"
TRANSFER_OPTION ||--o{ TRAVEL_SELECTION : "selected in"
TRAVEL_ADDON }o--o{ TRAVEL_SELECTION : "added to"
TRIP_PLAN {
uuid id PK
string caseId FK "UQ"
string corridorFrom
string corridorTo
int travellers
date departDate
date returnDate
int nights
}
FLIGHT_OPTION {
string id PK
string airline
string flightNo
int priceUsd
boolean recommended
}
STAY_OPTION {
string id PK
string name
string kind
int nightlyUsd
boolean recommended
}
TRAVEL_SELECTION {
uuid id PK
string caseId FK "NN"
string flightId FK
string stayId FK
string transferId FK
string status "CK selected|submitted|confirmed"
}TripPlan
- Purpose. The static trip parameters for a case (
TripPlan): corridor, travellers, dates, nights, surgery date. - Key attributes.
idPK;caseIdFK UQ;corridorFrom,corridorTo,travellers,travellerNote,departDate,returnDate,nights,surgeryDate.
FlightOption, StayOption, TransferOption, TravelAddon
- FlightOption.
idPK; airline,flightNo, from, to, depart, arrive, duration, stops, cabin,priceUsd,tags[],recommended. A bookable outbound flight for the corridor. - StayOption.
idPK;name,kind,distance,rating,nightlyUsd,tags[](attendant bed, halal kitchen),recommended. Accommodation near the hospital. - TransferOption.
idPK;name,blurb,priceUsd,note,recommended. Ground transport. - TravelAddon.
idPK;label,blurb,priceUsd,unit,defaultOn. Optional concierge add-on.
TravelSelection
- Purpose. The per-case record of what the patient chose and the Travel Desk confirmed.
- Key attributes.
idPK;caseIdFK NN;flightIdFK,stayIdFK,transferIdFK,addonIds[];statusCK (selected, submitted, confirmed). - Constraints. All prices are indicative until confirmed; confirmation calls the airline, hotel and transfer providers (see Integrations).
2.10 Domain: Payments and Escrow
Regulated, multi-currency money movement. A Quote becomes an EscrowAccount that schedules PaymentMilestones and records an append-only LedgerEntry stream; Refunds are compensating entries.
erDiagram
CASE ||--o{ QUOTE : "receives"
CASE ||--|| ESCROW_ACCOUNT : "funds"
ESCROW_ACCOUNT ||--o{ PAYMENT_MILESTONE : "schedules"
ESCROW_ACCOUNT ||--o{ LEDGER_ENTRY : "records"
PAYMENT_MILESTONE ||--o{ LEDGER_ENTRY : "releases via"
ESCROW_ACCOUNT ||--o{ REFUND : "issues"
QUOTE {
uuid id PK
string caseId FK "NN"
string clinicId FK
int totalMinor
string currency
string status "CK sent|accepted|declined|expired"
}
ESCROW_ACCOUNT {
uuid id PK
string caseId FK "UQ NN"
int totalMinor
int heldMinor
string currency
}
PAYMENT_MILESTONE {
uuid id PK
uuid escrowAccountId FK "NN"
string label
int amountMinor
string status "CK released|held|scheduled"
string date
}
LEDGER_ENTRY {
uuid id PK
uuid escrowAccountId FK "NN IDX"
uuid milestoneId FK
string desc
int amountMinor
string kind "CK out|rel"
timestamp createdAt "IDX"
}Quote
- Purpose. An itemised cost estimate sent to the patient by the clinic and coordinator.
- Key attributes.
idPK;caseIdFK NN;clinicIdFK;lines[](aCostLine[]grouped Treatment, Hospital, Travel & stay, Services);totalMinor,currency;statusCK (sent, accepted, declined, expired). - Constraints. Itemised, never a hidden markup; acceptance is the trigger to open an EscrowAccount.
EscrowAccount
- Purpose. Holds the patient's funds for one case (
Paymentsin the schema). - Key attributes.
idPK;caseIdFK UQ NN (one per case);totalMinor,heldMinor,currency. - Relationships. Schedules many
PaymentMilestones and records manyLedgerEntrys.
PaymentMilestone
- Purpose. A tranche released on a verified event (
PaymentMilestone). - Key attributes.
idPK;escrowAccountIdFK NN;label,amountMinor;statusCK (released, held, scheduled);date,note. - Indexes.
escrowAccountIdIDX;statusIDX (held and scheduled tranches). - Constraints. Moves to
releasedonly on its verified trigger (for example provider milestone confirmation), which posts a ledger entry.
LedgerEntry
- Purpose. The immutable money trail (
LedgerEntry). - Key attributes.
idPK;escrowAccountIdFK NN IDX;milestoneIdFK (when a release);desc;amountMinor(negative for money in or charged, positive for released);kindCK (out, rel);createdAtIDX. - Constraints. Append-only and immutable. Corrections are compensating entries, never edits or deletes. This is the auditable money trail finance reconciles against the payment partner.
Refund
idPK;escrowAccountIdFK NN;reason,amountMinor(omit for a full pre-arrival refund); posts a compensatingLedgerEntry. Pre-arrival refunds are full; post-milestone follow the disclosed schedule.
2.11 Domain: Communications
Secure messaging and notifications, both case-scoped.
erDiagram
CASE ||--o{ THREAD : "has"
THREAD ||--o{ MESSAGE : "contains"
CASE ||--o{ NOTIFICATION : "notifies"
THREAD {
uuid id PK
string caseId FK "NN"
string name
string role
int unread
boolean online
}
MESSAGE {
uuid id PK
uuid threadId FK "NN IDX"
boolean fromPatient
text body
json attachmentIds
timestamp createdAt "IDX"
}
NOTIFICATION {
uuid id PK
string caseId FK "NN"
string text
string tone "CK amber|green|teal"
string channel "CK in_app|whatsapp|email|sms"
timestamp createdAt
}Thread and Message
- Thread.
idPK;caseIdFK NN;name,role,initials,unread,online,last(preview). One per care-team conversation. - Message.
idPK;threadIdFK NN IDX;fromPatient(right-aligned bubble when true);body;attachmentIds[];createdAtIDX (for paging). Production is real-time with an audit trail.
Notification
idPK;caseIdFK NN;text,toneCK (amber, green, teal);channelCK (in_app, whatsapp, email, sms);createdAt; read state. Emitted by domain events (milestone, visa, travel, message) and delivered omnichannel, prioritising WhatsApp.
2.12 Domain: Growth and Agents
Source-country referrers and their milestone-gated, strictly non-clinical attribution.
erDiagram
AGENT ||--o{ REFERRAL : "submits"
REFERRAL }o--|| CASE : "attributes"
AGENT {
uuid id PK
string name
string referralCode UK
string tier "CK casual|agent|clinic"
string payoutAccount "masked"
}
REFERRAL {
uuid id PK
uuid agentId FK "NN IDX"
string caseId FK "IDX"
string milestoneStatus "CK lead|arrived|admitted|paid"
int payoutMinor
}Agent
idPK;name;referralCodeUQ;tierCK (casual, agent, clinic);payoutAccount(masked). A tracked referrer.
Referral
idPK;agentIdFK NN IDX;caseIdFK IDX (attribution);milestoneStatusCK (lead, arrived, admitted, paid);payoutMinor.- Constraints. Payouts gate on an arrived or admitted milestone, never on a raw lead, and are a marketing fee from Global Clinic's margin, never a clinical kickback (see the compliance boundary).
2.13 Audit, consent and data lifecycle
A cross-cutting domain. Every clinical, financial, consent and visa action is recorded immutably; consent is a first-class ledger; deletion is governed by retention and legal holds.
erDiagram
CASE ||--o{ AUDIT_EVENT : "records"
CASE ||--o{ CONSENT : "tracks"
USER ||--o{ AUDIT_EVENT : "acts in"
AUDIT_EVENT {
uuid id PK
string caseId FK "IDX"
string entity "IDX"
string entityId
string action
uuid actorId FK "IDX"
string actorRole
json before
json after
string ip
timestamp createdAt "IDX"
}
CONSENT {
uuid id PK
string caseId FK "IDX"
uuid subjectId FK
string purpose
string scope
string state "CK granted|withdrawn"
timestamp grantedAt
timestamp withdrawnAt
}AuditEvent
- Purpose. The append-only record of who did what to which entity, with before and after state.
- Key attributes.
idPK;caseIdFK IDX;entityIDX,entityId,action;actorIdFK IDX,actorRole;before,after(JSON snapshots);ip;createdAtIDX. - Indexes.
caseId,actorId,createdAt, and composite(entity, entityId)for per-record history. - Covers. Document verification, escrow funding and release, milestone confirmation, refunds, visa submission and grant, consent grant and withdrawal, role and access changes, and case status changes.
- Integrity. Immutable and tamper-evident; retained for the longest applicable regime.
Consent
- Purpose. The system of record for privacy compliance and DSAR handling.
- Key attributes.
idPK;caseIdFK IDX;subjectIdFK (patient or attendant);purpose,scope;stateCK (granted, withdrawn);grantedAt,withdrawnAt. - Constraints. Granular and purpose-bound (clinical review, hospital sharing, travel and visa, marketing); marketing consent is never bundled with service consent; withdrawal stops the relevant processing prospectively.
Data lifecycle
- Soft delete. Operational rows carry
deletedAt; history is preserved. - Legal hold. Pauses retention-driven deletion.
- Hard deletion runs only on the retention schedule and is itself audited.
- Tracking timestamps.
createdAt/updatedAton every mutable entity; lifecycle event timestamps (enteredAt,submittedAt,grantedAt,releasedAt) drive SLA measurement and reporting.
2.14 Cross-entity validation and referential integrity
These rules span domains and are the authoritative source for QA and service-layer validation.
- Stage pointer.
Case.currentStageis 0 to 13 and forward-only; regressions require an explicit, audited override. - Visa submit gate. A
VisaApplicationmay be submitted only when everyVisaChecklistItemresolves to done (linked items via a verifiedDocument). - Document verification. Only an authorised reviewer sets
Document.status = verified; patients cannot self-verify. - Escrow release. A
PaymentMilestonemoves toreleasedonly on its verified trigger and posts an immutableLedgerEntry. - Refund policy. Pre-arrival refunds are full; post-milestone refunds follow the disclosed schedule.
- Clinic visibility. A
Clinicis publicly listed only when itsClinicAccount.status = verified. - Attendant count. At most two attendants per case (e-Medical Attendant visa).
- Currency consistency. All monetary amounts on a case use the corridor currency; cross-currency conversions are recorded with rate and timestamp.
- Referential integrity.
caseId,clinicId,corridorId,procedureandspecialtyforeign keys must resolve; orphan rows are rejected. Deletes are soft, so foreign keys remain valid through history.
2.15 Indexing strategy (consolidated)
Indexes follow the dominant access patterns: fetch a case and its children, a clinic's queue, a corridor's cases, and reporting. The per-domain tables above mark indexes inline; the consolidated set:
| Table | Index | Rationale |
|---|---|---|
| case | PK caseId; IDX patientId, clinicId, corridorId, coordinatorId, status | Case lookup and per-owner queues |
| stage_progress | IDX caseId; (caseId, status); dueAt | Active stage and SLA-breach queries |
| document | IDX caseId; (caseId, type); partial status = pending | Wallet by case; verification queue |
| visa_checklist_item | IDX visaApplicationId, docId | Resolve linked-document completion |
| payment_milestone | IDX escrowAccountId, status | Held and scheduled tranches |
| ledger_entry | IDX escrowAccountId, createdAt; append-only | Chronological, immutable ledger |
| clinic_inquiry | IDX clinicId; (clinicId, status); date | Clinic inbox by status |
| clinic_case | IDX clinicId; (clinicId, stage) | Clinic case board |
| message | IDX threadId, createdAt | Thread paging |
| review | IDX clinicId, verified, createdAt | Clinic reviews, verified-first |
| clinic | PK id; GIN specialties, accreditations; IDX country, priceFromUsd; partial vetted = true | Marketplace filters |
| procedure | PK key; IDX specialty, priceFromUsd | Catalogue filters |
| referral | IDX agentId, caseId, milestoneStatus | Attribution and payouts |
| audit_event | IDX caseId, actorId, createdAt; (entity, entityId) | Audit retrieval |
Tenant scoping: composite indexes lead with the tenant key (clinicId) or caseId so row-level-security-scoped queries stay selective. Full-text marketplace search uses a generated tsvector with a GIN index. Time-series reporting is offloaded to the analytics warehouse rather than indexed on the operational store.
2.16 Reference data versus transactional data
- Reference and configuration data (Stage, Specialty, Procedure, Accreditation, Corridor, CountryPack, the catalogue FlightOption/StayOption/TransferOption) changes rarely and is read constantly. Cache it aggressively; version it where rules matter (Country Packs are versioned and feature-flagged).
- Transactional data (Case and its children: documents, consultations, visa, escrow, ledger, messages) is written constantly and is strictly tenant- and case-scoped, audited, and soft-deleted.
- Derived and materialized data (Reputation aggregates, savings percentages, reporting rollups) is computed from the above and rebuilt on change or on a schedule, never hand-edited.
This split guides where to put load (cache reference data, scale the operational store, offload analytics to the warehouse) and how to reason about consistency.