Skip to content

System Fields

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_persons — the patient's portable profile — and are shared across all orgs the patient registers at:

DemographicTableColumn
Date of birthpatient_personsdate_of_birth
Sexpatient_personssex
Occupationpatient_personsoccupation
Residencepatient_personsresidence

For PDF templates and exports: read these values by joining patients → patient_persons 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_persons)

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

go
// PDF template: "put date of birth at position (x, y)"
// Read from patient_persons, 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 BIGINT) 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_persons.
    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_persons 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_persons columnssystem_key lookupcustom_field_id
CSV exportspatient_persons columnssystem_key column headersField key
Forms (auto-fill)profile_field_key in templatecustom_field_idcustom_field_id
Forms (profile sync)Write to patient_personsWrite to custom_field_valuesWrite to custom_field_values
SegmentsQuery patient_persons 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;