Skip to content

Custom Fields — Org-Specific Field Library

Define org-specific fields once, use them in forms and profiles, update them without breaking historical records.


What this enables

Org-specific extensibility: Every clinic has different needs. A sports clinic tracks "preferred training surface". A physio clinic tracks "referral source". Custom fields let each org extend patient, specialist, appointment, and organization records with fields the platform doesn't model natively.

Non-breaking field updates: Admin adds a new option to a select field? Old form submissions keep the original options (snapshot). New forms automatically get the updated options.

Profile syncing: When a patient fills a form field that is linked to a custom field, their profile value updates automatically — pre-filling future forms.

Smart patient segmentation: Create segments like "All patients referred by a physiotherapist" by querying custom field values.


What belongs here vs the patient profile

Before creating a custom field for a patient, ask: Is this a universal fact about the person, or is it specific to how our clinic tracks them?

DataWhere it livesShared across orgs?
Date of birth, sex, phonepatient_persons (portable profile)✅ Yes
Blood type, allergies, chronic conditionspatient_persons (portable profile)✅ Yes
Occupation, residence, emergency contact, insurancepatient_persons (portable profile)✅ Yes
Referral source, VIP status, training surface, billing notesCustom fields (org-scoped)❌ No

Custom fields are for org-specific extras — the things that genuinely belong to the clinic's context, not to the patient as a universal person. See Patient Profile → for the portable profile model.


How it works

  1. Admin creates a field: "Referral Source" with options: Physiotherapist, GP, Online, Word of mouth
  2. Form template references it: Intake form includes "Referral Source" without duplicating the definition
  3. Patient fills form: Answer is snapshotted with field v1's options
  4. Admin updates field: Adds "Social Media" option → published as v2
  5. New patients: See the updated options automatically
  6. Old patients: Their submitted forms still show the original options (historical accuracy)
  7. Profile sync: Latest answer appears in patient's org profile, pre-fills next form

Technical Reference

Everything below is intended for developers.

Entity types

Custom fields can be defined for any entity:

Entity typeUse caseExamples
patientOrg-specific patient attributesReferral source, VIP status, billing notes, training preference
specialistProvider credentialsMedical license #, certifications, languages
appointmentAppointment metadataInternal priority, billing code, room number
organizationOrg-level configurationCustom settings not modeled in the orgs table

Versioning & immutability

Custom fields are versioned on publish to ensure historical integrity:

Day 1: Admin creates "referral_source" field
  - type: select
  - options: ["Physiotherapist", "GP", "Online", "Word of mouth"]
  - version: 1, published: true

Day 10: Patient fills form → snapshots field v1

Day 30: Admin edits "referral_source" → adds "Social Media"
  - Archives v1 to custom_field_versions
  - Publishes v2 with 5 options

Result:
  - Old form still shows 4 options (v1 snapshot)
  - New forms automatically get 5 options (v2)

Linking form fields to custom fields vs the portable profile

A form template field can be linked in two ways:

MechanismWhat it links toProfile sync
custom_field_idAn org-scoped custom fieldWrites to custom_field_values
profile_field_keyA patient_persons columnWrites to patient_persons directly

Use custom_field_id for org-specific fields. Use profile_field_key for fields that represent universal patient facts (date of birth, blood type, etc.). A field can have one or the other, never both.

See Form Auto-Fill → for the full mechanics.

Data model

custom_fields (current published version)
  ├── id
  ├── organization_id
  ├── entity_type ('patient' | 'specialist' | 'appointment' | 'organization')
  ├── key            (admin-chosen identifier, localizable)
  ├── label          (display text, localizable)
  ├── field_type     (text | textarea | select | date | checkbox | radio | number | email | phone)
  ├── options        (for select/radio/checkbox)
  ├── is_private     (specialist-only visibility in PDFs)
  ├── system_key     (stable ID for system-critical fields — NULL for org-created fields)
  └── sort_order

custom_field_versions (historical snapshots)
  ├── custom_field_id
  ├── version
  ├── fields_snapshot (JSONB — full field definition at that version)
  └── published_at

custom_field_values (per-entity data)
  ├── organization_id
  ├── custom_field_id → custom_fields.id
  ├── entity_type    (matches custom_fields.entity_type)
  ├── entity_id      (patients.id | specialists.id | appointments.id | organizations.id)
  └── value          (plaintext TEXT — queryable for segments)

One-off form fields

What if a form needs a field that should not sync to any profile? (e.g., "Chief complaint today" — appointment-specific):

json
{
  "custom_field_id": null,
  "profile_field_key": null,
  "key": "chief_complaint_today",
  "type": "textarea",
  "label": "What brings you in today?"
}

Values are saved in forms.values only. No profile sync, no reuse.

System fields

Some custom fields have a stable system_key for use by PDF templates and external integrations. The system_key never changes even if the admin renames the field's key or label for localization. See System Fields →.

Storage

All custom field values are stored as plaintext TEXT:

  • Infrastructure encryption (AWS RDS AES-256) covers HIPAA at-rest requirement
  • Fully queryable for segments and filtering
  • is_private controls visibility in documents, not access during form filling

Examples

Specialty-specific fields

Sports clinic:

json
{"key": "training_surface", "label": "Preferred Training Surface",
 "field_type": "select", "options": ["Court", "Grass", "Artificial turf", "Sand"]}

Dermatology clinic:

json
{"key": "skin_type", "label": "Skin Type",
 "field_type": "select", "options": ["I", "II", "III", "IV", "V", "VI"]}

Internal tracking

json
{"key": "referral_source", "label": "How did you hear about us?", "field_type": "select",
 "options": ["Physiotherapist", "GP", "Online", "Word of mouth"]}

{"key": "vip_status", "label": "VIP Patient", "field_type": "checkbox", "is_private": true}

Localization of system fields

system_key: "patient_insurance_number"  ← stable, referenced by PDF templates
key: "numar-asigurare"                  ← admin renamed to Romanian
label: "Număr asigurare"                ← admin renamed to Romanian

Documentation structure

  • index.md (this file) — Overview and field library model
  • schema.sql — Tables, indexes, RLS policies
  • api.md — HTTP endpoints and request/response formats
  • versioning.md — Detailed versioning workflow and snapshot logic
  • system-fields.md — System field concepts and PDF template mapping
  • entity-profiles.md — Org-specific profile data and auto-fill integration
  • Patients (../patients/) — Portable profile model (patient_persons)
  • Forms (../forms/) — Reference custom fields in templates, snapshot versions in instances
  • Segments (../segments/) — Query custom_field_values for patient cohorts