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:
| Demographic | Table | Column |
|---|---|---|
| Date of birth | patient_persons | date_of_birth |
| Sex | patient_persons | sex |
| Occupation | patient_persons | occupation |
| Residence | patient_persons | residence |
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:
| Organization | Field purpose | Custom key | Custom label |
|---|---|---|---|
| US Clinic | Insurance number | ins_id | Insurance ID |
| Romanian Clinic | Insurance number | numar-asigurare | Număr asigurare |
| Spanish Clinic | Insurance number | num-seguro | Nú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 renamedPDF templates reference system_key="patient_insurance_number" — they find the right field regardless of the org's localization.
System fields vs custom fields
| System fields | Custom fields | |
|---|---|---|
system_key | Non-NULL (e.g., patient_insurance_number) | NULL |
| Created by | Auto-seeded on org creation | Admin creates via API |
key + label | Editable (localization) | Editable |
| Deletable | No (API rejects) | Yes |
system_key editable | No (API rejects) | N/A |
| Used by | PDF templates, exports, integrations | Forms, profiles, segments |
How PDF templates use system fields
For org-specific system fields (custom_field_values)
// 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 fieldFor portable profile fields (patient_persons)
Date of birth, sex, occupation, and residence are now read directly from patient_persons — not from custom_field_values:
// 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:
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 OKWhere fields are referenced — summary
| Context | Patient demographics (DOB, sex, etc.) | Org-specific system fields | Org custom fields |
|---|---|---|---|
| PDF templates | Read from patient_persons columns | system_key lookup | custom_field_id |
| CSV exports | patient_persons columns | system_key column headers | Field key |
| Forms (auto-fill) | profile_field_key in template | custom_field_id | custom_field_id |
| Forms (profile sync) | Write to patient_persons | Write to custom_field_values | Write to custom_field_values |
| Segments | Query patient_persons directly | custom_field_id | custom_field_id |
Adding new system fields
If a new integration or PDF template needs a stable field identifier:
- Add the field to the seeding function with a
system_key - Run a migration to seed existing organizations
- Update PDF templates or exports to reference the new
system_key
-- 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;Related documentation
- Custom Fields Overview — Field library model
- Versioning — How fields are versioned
- Entity Profiles — Profile data and auto-fill
- Patients → — Portable patient profile (
patient_persons)