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?
| Data | Where it lives | Shared across orgs? |
|---|---|---|
| Date of birth, sex, phone | patient_persons (portable profile) | ✅ Yes |
| Blood type, allergies, chronic conditions | patient_persons (portable profile) | ✅ Yes |
| Occupation, residence, emergency contact, insurance | patient_persons (portable profile) | ✅ Yes |
| Referral source, VIP status, training surface, billing notes | Custom 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
- Admin creates a field: "Referral Source" with options: Physiotherapist, GP, Online, Word of mouth
- Form template references it: Intake form includes "Referral Source" without duplicating the definition
- Patient fills form: Answer is snapshotted with field v1's options
- Admin updates field: Adds "Social Media" option → published as v2
- New patients: See the updated options automatically
- Old patients: Their submitted forms still show the original options (historical accuracy)
- 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 type | Use case | Examples |
|---|---|---|
patient | Org-specific patient attributes | Referral source, VIP status, billing notes, training preference |
specialist | Provider credentials | Medical license #, certifications, languages |
appointment | Appointment metadata | Internal priority, billing code, room number |
organization | Org-level configuration | Custom 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:
| Mechanism | What it links to | Profile sync |
|---|---|---|
custom_field_id | An org-scoped custom field | Writes to custom_field_values |
profile_field_key | A patient_persons column | Writes 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):
{
"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_privatecontrols visibility in documents, not access during form filling
Examples
Specialty-specific fields
Sports clinic:
{"key": "training_surface", "label": "Preferred Training Surface",
"field_type": "select", "options": ["Court", "Grass", "Artificial turf", "Sand"]}Dermatology clinic:
{"key": "skin_type", "label": "Skin Type",
"field_type": "select", "options": ["I", "II", "III", "IV", "V", "VI"]}Internal tracking
{"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 RomanianDocumentation 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
Related features
- 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