Registration Service — Data Model
Status: populated Owner: TBD Last updated: 2026-04-17 Companion: Service Template · NAMING
1. ID Prefix Registry
| Prefix | Entity |
|---|---|
pat_ | Patient (external reference; internal PK is UUID) |
enc_ | Encounter |
nok_ | NextOfKin entry |
cfl_ | ConsentFlag |
prt_ | PatientPortrait |
ext_ | PatientExtensionInstance |
sch_ | ExtensionSchema row |
2. TypeScript Interfaces
// domain/patient.ts
export interface PatientIdentifier {
id: string;
patientId: string;
tenantId: string;
system: string;
value: string;
typeCode: string;
issuer?: string;
verificationStatus?: 'unverified' | 'pending' | 'verified' | 'rejected';
confidence?: number; // 0–1
active: boolean;
}
export interface PatientName {
id: string;
patientId: string;
use: 'official' | 'usual' | 'temp' | 'nickname' | 'anonymous' | 'old' | 'maiden';
family: string;
given: string[];
middle?: string;
patronymic?: string;
script: 'latn' | 'arab' | 'arab-af' | 'fa-af' | 'ps';
textDirection: 'ltr' | 'rtl';
periodStart?: string;
periodEnd?: string;
}
export interface PatientAggregate {
id: string; // UUID
stablePatientId: string; // same UUID — explicit for integrators
tenantId: string;
mrn: string;
isUnidentified: boolean;
isProvisional: boolean;
isNewborn: boolean;
newbornLinkedPatientId?: string;
newbornLinkRole?: 'MOTHER' | 'GUARDIAN';
temporaryEnterpriseKey?: string;
unidentifiedSlaDueAt?: string;
intakeContext?: Record<string, string>;
birthDate?: string; // YYYY-MM-DD
sex?: 'M' | 'F' | 'O' | 'U';
preferredLang?: string;
nkda: boolean; // No Known Drug Allergies flag
isActive: boolean;
deceased: boolean;
deceasedDateTime?: string;
deceasedRecordedBy?: string;
deceasedRecordedAt?: string;
mergedIntoPatientId?: string;
createdBy: string;
version: number;
createdAt: string;
updatedAt: string;
names: PatientName[];
identifiers: PatientIdentifier[];
telecoms: PatientTelecom[];
addresses: PatientAddress[];
nextOfKin: NextOfKinEntry[];
consentFlags: ConsentFlag[];
extensionInstances: PatientExtensionInstance[];
}
3. PostgreSQL Schema
-- Patients
CREATE TABLE patients (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
mrn TEXT NOT NULL,
is_unidentified BOOLEAN NOT NULL DEFAULT FALSE,
is_provisional BOOLEAN NOT NULL DEFAULT FALSE,
is_newborn BOOLEAN NOT NULL DEFAULT FALSE,
newborn_linked_patient_id UUID,
newborn_link_role TEXT,
temporary_enterprise_key TEXT,
unidentified_sla_due_at TIMESTAMPTZ,
intake_context JSONB,
birth_date DATE,
sex CHAR(1),
preferred_lang TEXT,
nkda BOOLEAN NOT NULL DEFAULT FALSE,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
deceased BOOLEAN NOT NULL DEFAULT FALSE,
deceased_date_time TIMESTAMPTZ,
deceased_recorded_by UUID,
deceased_recorded_at TIMESTAMPTZ,
merged_into_patient_id UUID,
created_by UUID NOT NULL,
version INTEGER NOT NULL DEFAULT 1,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX ix_patients_tenant_mrn ON patients (tenant_id, mrn);
CREATE INDEX ix_patients_tenant_active ON patients (tenant_id, is_active);
CREATE INDEX ix_patients_merged_into ON patients (merged_into_patient_id) WHERE merged_into_patient_id IS NOT NULL;
-- Patient names (pg_trgm for fuzzy search)
CREATE TABLE patient_names (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
patient_id UUID NOT NULL REFERENCES patients(id),
tenant_id UUID NOT NULL,
use TEXT NOT NULL DEFAULT 'official',
family TEXT,
given TEXT[],
middle TEXT,
patronymic TEXT,
script TEXT NOT NULL DEFAULT 'latn',
text_direction TEXT NOT NULL DEFAULT 'ltr',
period_start DATE,
period_end DATE
);
CREATE INDEX ix_patient_names_patient_id ON patient_names (patient_id);
CREATE INDEX ix_patient_names_trgm_family ON patient_names USING gin (family gin_trgm_ops);
-- Patient identifiers
CREATE TABLE patient_identifiers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
patient_id UUID NOT NULL REFERENCES patients(id),
tenant_id UUID NOT NULL,
system TEXT NOT NULL,
value TEXT NOT NULL,
type_code TEXT,
issuer TEXT,
verification_status TEXT,
confidence NUMERIC(3,2),
active BOOLEAN NOT NULL DEFAULT TRUE
);
CREATE UNIQUE INDEX ix_patient_identifiers_tenant_system_value
ON patient_identifiers (tenant_id, system, value) WHERE active = TRUE;
-- Patient telecoms
CREATE TABLE patient_telecoms (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
patient_id UUID NOT NULL REFERENCES patients(id),
tenant_id UUID NOT NULL,
system TEXT NOT NULL,
value TEXT NOT NULL,
use TEXT,
rank INTEGER
);
-- Patient addresses
CREATE TABLE patient_addresses (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
patient_id UUID NOT NULL REFERENCES patients(id),
tenant_id UUID NOT NULL,
country TEXT,
state_province TEXT,
district TEXT,
city TEXT,
line TEXT[],
postal_code TEXT,
use TEXT
);
-- Next-of-kin
CREATE TABLE patient_next_of_kin (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
patient_id UUID NOT NULL REFERENCES patients(id),
tenant_id UUID NOT NULL,
relationship TEXT NOT NULL,
name TEXT,
phone TEXT,
related_patient_id UUID,
priority INTEGER DEFAULT 0,
active BOOLEAN NOT NULL DEFAULT TRUE
);
CREATE INDEX ix_nok_patient_id ON patient_next_of_kin (patient_id);
CREATE INDEX ix_nok_related_patient ON patient_next_of_kin (related_patient_id) WHERE related_patient_id IS NOT NULL;
-- Consent flags
CREATE TABLE patient_consent_flags (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
patient_id UUID NOT NULL REFERENCES patients(id),
tenant_id UUID NOT NULL,
type TEXT NOT NULL,
granted BOOLEAN NOT NULL,
channel TEXT
);
-- Encounters
CREATE TABLE encounters (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
patient_id UUID NOT NULL REFERENCES patients(id),
encounter_type TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'planned',
facility_id UUID,
location_id UUID,
attending_id UUID,
admitted_at TIMESTAMPTZ,
discharged_at TIMESTAMPTZ,
chief_complaint TEXT,
created_by UUID NOT NULL,
version INTEGER NOT NULL DEFAULT 1,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX ix_encounters_tenant_patient ON encounters (tenant_id, patient_id);
CREATE INDEX ix_encounters_status ON encounters (tenant_id, status);
-- Vital status corrections audit table
CREATE TABLE patient_vital_status_corrections (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
patient_id UUID NOT NULL,
tenant_id UUID NOT NULL,
prior_deceased BOOLEAN,
prior_deceased_date_time TIMESTAMPTZ,
new_deceased BOOLEAN,
new_deceased_date_time TIMESTAMPTZ,
corrected_by UUID NOT NULL,
corrected_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Outbox
CREATE TABLE outbox (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
aggregate_id UUID NOT NULL,
event_type TEXT NOT NULL,
payload JSONB NOT NULL,
published BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX ix_outbox_unpublished ON outbox (created_at) WHERE published = FALSE;
4. Row-Level Security
ALTER TABLE patients ENABLE ROW LEVEL SECURITY;
CREATE POLICY patients_tenant_isolation ON patients
USING (tenant_id = current_setting('app.tenant_id')::uuid);
-- Same policy pattern applied to: patient_names, patient_identifiers,
-- patient_telecoms, patient_addresses, patient_next_of_kin,
-- patient_consent_flags, encounters, outbox
5. Extension Schema Tables
CREATE TABLE extension_schemas (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
bundle_key TEXT NOT NULL,
schema_version INTEGER NOT NULL DEFAULT 1,
json_schema JSONB NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX ix_ext_schema_tenant_bundle_active
ON extension_schemas (tenant_id, bundle_key) WHERE is_active = TRUE;
CREATE TABLE patient_extension_instances (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
patient_id UUID NOT NULL,
tenant_id UUID NOT NULL,
bundle_key TEXT NOT NULL,
schema_id UUID NOT NULL REFERENCES extension_schemas(id),
payload JSONB NOT NULL,
effective_from DATE NOT NULL,
effective_to DATE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX ix_ext_inst_patient_bundle ON patient_extension_instances (patient_id, bundle_key, effective_from DESC);
6. Portrait Storage
CREATE TABLE patient_portraits (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
patient_id UUID NOT NULL,
tenant_id UUID NOT NULL,
content_type TEXT NOT NULL,
storage_key TEXT NOT NULL, -- object store path (encrypted at rest)
consent_given BOOLEAN NOT NULL,
clinical_display_allowed BOOLEAN NOT NULL DEFAULT TRUE,
retention_until TIMESTAMPTZ NOT NULL,
superseded_portrait_id UUID,
created_by UUID NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX ix_portraits_patient_active ON patient_portraits (patient_id, created_at DESC);