Skip to content

System Fields ​

NOT BUILT — reconciled 2026-08-02

No custom_fields / custom_field_versions / custom_field_values table exists. Ships in migration 000042. See index.md for the full correction list. The load-bearing ones: patient_profiles, not patient_profiles (and the RLS helper is current_human_patient_profile_ids()); UNIQUE (organization_id, entity_type, key) — never global; UUID PKs, not integers; there is no users table; and a national_id (CNP) field never reaches custom_field_values.value — it routes to the encrypted patient_profiles column or is rejected.

System fields are custom fields with a stable system_key identifier, used by PDF templates and external integrations to reference org-specific data regardless of how the org has named or labelled the field.


Context: what is NOT a system field ​

Basic patient demographics (date of birth, sex, occupation, residence) are not custom fields. They are native columns on patient_profiles — the patient's portable profile — and are shared across all orgs the patient registers at:

DemographicTableColumn
Date of birthpatient_profilesdate_of_birth
Sexpatient_profilessex
Occupationpatient_profilesoccupation
Residencepatient_profilesresidence

For PDF templates and exports: read these values by joining patients → patient_profiles directly, not via custom_field_values. See the PDF resolution example below.

The system_key mechanism is retained for any org-specific fields that integrations, exports, or PDF templates need to reference by a stable identifier — regardless of how the admin has customized the field's key or label.


The problem system fields solve ​

PDF templates have fixed positions for specific data points. But custom field key and label are freeform — different orgs name them differently:

OrganizationField purposeCustom keyCustom label
US ClinicInsurance numberins_idInsurance ID
Romanian ClinicInsurance numbernumar-asigurareNumăr asigurare
Spanish ClinicInsurance numbernum-seguroNúmero de seguro

A PDF template can't hardcode key='ins_id' — that won't work for the Romanian clinic.

Solution: system_key ​

system_key is a stable identifier that never changes, even when admins customize key and label.

Before (default):
  system_key: "patient_insurance_number"   ← unchanged, referenced by PDFs
  key: "insurance_number"
  label: "Insurance Number"

After (Romanian localization):
  system_key: "patient_insurance_number"   ← unchanged
  key: "numar-asigurare"                   ← admin renamed
  label: "Număr asigurare"                 ← admin renamed

PDF templates reference system_key="patient_insurance_number" — they find the right field regardless of the org's localization.


System fields vs custom fields ​

System fieldsCustom fields
system_keyNon-NULL (e.g., patient_insurance_number)NULL
Created byAuto-seeded on org creationAdmin creates via API
key + labelEditable (localization)Editable
DeletableNo (API rejects)Yes
system_key editableNo (API rejects)N/A
Used byPDF templates, exports, integrationsForms, profiles, segments

How PDF templates use system fields ​

For org-specific system fields (custom_field_values) ​

go
// PDF template: "put insurance number at position (x, y)"
// Service resolves via system_key, not key/label:
field, _ := customFieldRepo.FindBySystemKey(ctx, orgID, "patient_insurance_number")
value, _ := customFieldValueRepo.FindByField(ctx, field.ID, "patient", patientID)
// value.Value = "AXA-123456" — regardless of what the org named the field

For portable profile fields (patient_profiles) ​

Date of birth, sex, occupation, and residence are now read directly from patient_profiles — not from custom_field_values:

go
// PDF template: "put date of birth at position (x, y)"
// Read from patient_profiles, joined through patients:
person, _ := patientPersonRepo.GetByPatientID(ctx, patientID)
// person.DateOfBirth = "1990-05-15"

renderAtPosition(pdfField.Position, person.DateOfBirth.Format("DD.MM.YYYY"))

No system_key lookup needed — these are typed columns, not key-value pairs.


Seeding system fields ​

System fields are auto-created when an organization is created. For org-specific fields that all orgs should share (e.g. insurance number, national ID), the seed function inserts them:

sql
CREATE OR REPLACE FUNCTION seed_system_custom_fields(org_id UUID) RETURNS VOID AS $$
BEGIN
    -- Add any org-specific system fields here.
    -- Basic patient demographics (date_of_birth, sex, occupation, residence)
    -- are NOT seeded as custom fields — they live on patient_profiles.
    INSERT INTO custom_fields (organization_id, entity_type, key, label, field_type, system_key, sort_order)
    VALUES
        -- Example: insurance number as a system field (org-scoped, but stable key for PDFs)
        -- (org_id, 'patient', 'insurance_number', 'Insurance Number', 'text', 'patient_insurance_number', 1)
    ON CONFLICT (organization_id, system_key) DO NOTHING;
END;
$$ LANGUAGE plpgsql;

Patient demographics (date_of_birth, residence, occupation, sex) are native columns on patient_profiles and are not seeded as custom fields.


API protection ​

Cannot delete system fields ​

DELETE /v1/custom-fields/3
→ 403 Forbidden: "Cannot delete system field"

Cannot change system_key ​

PATCH /v1/custom-fields/3  {"system_key": "new_key"}
→ 400 Validation Error: "system_key is immutable"

Can edit key and label (for localization) ​

PATCH /v1/custom-fields/3  {"key": "numar-asigurare", "label": "Număr asigurare"}
→ 200 OK

Where fields are referenced — summary ​

ContextPatient demographics (DOB, sex, etc.)Org-specific system fieldsOrg custom fields
PDF templatesRead from patient_profiles columnssystem_key lookupcustom_field_id
CSV exportspatient_profiles columnssystem_key column headersField key
Forms (auto-fill)profile_field_key in templatecustom_field_idcustom_field_id
Forms (profile sync)Write to patient_profilesWrite to custom_field_valuesWrite to custom_field_values
SegmentsQuery patient_profiles directlycustom_field_idcustom_field_id

Adding new system fields ​

If a new integration or PDF template needs a stable field identifier:

  1. Add the field to the seeding function with a system_key
  2. Run a migration to seed existing organizations
  3. Update PDF templates or exports to reference the new system_key
sql
-- Migration: seed new system field for existing orgs
INSERT INTO custom_fields (organization_id, entity_type, key, label, field_type, system_key, sort_order)
SELECT id, 'patient', 'national_id', 'National ID', 'text', 'patient_national_id', 2
FROM organizations
ON CONFLICT (organization_id, system_key) DO NOTHING;