Skip to content

Custom Field Versioning

Overview

Custom field versioning ensures that form instances preserve their exact field definitions at the time of creation, even when field schemas are later updated. This is critical for HIPAA compliance - signed forms must be immutable.

Why Versioning?

ScenarioWithout VersioningWith Versioning
Admin adds option to select fieldOld forms retroactively show new option (wrong!)Old forms preserve original options (correct)
Admin changes field typeData corruption, validation failuresOld forms keep original type
Audit/compliance reviewCan't prove what form looked like when signedExact snapshot preserved forever
Template rollbackLost foreverCan restore any previous version

Version Lifecycle

1. Initial Creation

bash
POST /v1/custom-fields
{
  "entity_type": "patient",
  "key": "blood_type",
  "label": "Blood Type",
  "field_type": "select",
  "options": ["A+", "A-", "B+", "B-", "O+", "O-"]
}

Response:
{
  "id": 42,
  "version": 1,
  "published": true,
  "published_at": "2025-01-15T10:00:00Z"
}

Backend actions:

  1. Create custom_fields row (version: 1, published: true)
  2. Archive to custom_field_versions:
    sql
    INSERT INTO custom_field_versions (
      custom_field_id, version, fields_snapshot, published_at
    ) VALUES (
      42, 1,
      '{"key":"blood_type","label":"Blood Type","field_type":"select",...}',
      '2025-01-15T10:00:00Z'
    )

2. Form Instance Snapshots Field Version

When form instance created:
  1. Read custom_field from form_template
  2. Lookup current published version (v1)
  3. Snapshot into form_instance.fields:
     {
       "custom_field_id": 42,
       "version": 1,           ← Links to custom_field_versions
       "key": "blood_type",
       "label": "Blood Type",
       "field_type": "select",
       "options": ["A+", "A-", "B+", "B-", "O+", "O-"]
     }

This snapshot is immutable. Even if the custom field is later deleted, the form instance preserves its field definition forever.

3. Editing (Draft State)

bash
PATCH /v1/custom-fields/42
{
  "options": ["A+", "A-", "B+", "B-", "O+", "O-", "AB+", "AB-"]
}

Response:
{
  "id": 42,
  "version": 1, Still v1
  "published": false, Now in draft state
  "published_at": "2025-01-15T10:00:00Z"
}

Backend actions:

  1. Set published = false (draft state)
  2. Update fields in-place (mutable while draft)
  3. Forms still use v1 (last published version from custom_field_versions)

4. Publishing New Version

bash
POST /v1/custom-fields/42/publish

Response:
{
  "id": 42,
  "version": 2, Incremented
  "published": true,
  "published_at": "2025-02-01T14:30:00Z"
}

Backend actions:

  1. Archive v1 to custom_field_versions (if not already archived)
  2. Archive current draft as v2:
    sql
    INSERT INTO custom_field_versions (
      custom_field_id, version, fields_snapshot, published_at
    ) VALUES (
      42, 2,
      '{"key":"blood_type","options":["A+","A-",...,"AB+","AB-"]}',
      '2025-02-01T14:30:00Z'
    )
  3. Update custom_fields: version = 2, published = true

Result:

  • Old form instances: still snapshot v1 (6 options)
  • New form instances: snapshot v2 (8 options)
  • Templates: unchanged (reference field_id: 42, version determined at form creation)

Snapshot Resolution

When creating a form instance, the backend resolves field versions:

sql
-- Get current published version
SELECT version, fields_snapshot
FROM custom_field_versions
WHERE custom_field_id = 42
  AND version = (SELECT version FROM custom_fields WHERE id = 42 AND published = true)

-- Or if custom_field is in draft state, use last published version
SELECT version, fields_snapshot
FROM custom_field_versions
WHERE custom_field_id = 42
ORDER BY version DESC
LIMIT 1

Key rule: Always use the latest published version at the time of form instance creation.

Version History API

Get All Versions

bash
GET /v1/custom-fields/42/versions

Response:
[
  {
    "version": 2,
    "published_at": "2025-02-01T14:30:00Z",
    "fields": {
      "key": "blood_type",
      "label": "Blood Type",
      "field_type": "select",
      "options": ["A+", "A-", "B+", "B-", "O+", "O-", "AB+", "AB-"]
    }
  },
  {
    "version": 1,
    "published_at": "2025-01-15T10:00:00Z",
    "fields": {
      "key": "blood_type",
      "label": "Blood Type",
      "field_type": "select",
      "options": ["A+", "A-", "B+", "B-", "O+", "O-"]
    }
  }
]

Get Specific Version

bash
GET /v1/custom-fields/42/versions/1

Response:
{
  "version": 1,
  "published_at": "2025-01-15T10:00:00Z",
  "fields": {...}
}

Rollback to Previous Version

bash
POST /v1/custom-fields/42/rollback
{
  "version": 1
}

Backend:
1. Load version 1 snapshot from custom_field_versions
2. Restore fields to custom_fields (published = false, draft state)
3. Admin can edit if needed, then publish as v3

Draft vs Published Workflow

StateForms BehaviorAdmin Actions
DraftUse last published versionCan edit freely
PublishedUse current published versionRead-only (must edit → draft → publish)

Example workflow:

v1 published
  └─ Forms use v1

Admin edits → draft state
  └─ Forms STILL use v1 (last published)

Admin publishes → v2 published
  └─ New forms use v2, old forms keep v1

Admin edits v2 → draft state
  └─ Forms STILL use v2 (last published)

Immutability Guarantees

Form Instances

Once a form instance is created:

  • Field definitions are frozen (type, options, label, validation)
  • Even if custom field is deleted, form instance preserves the snapshot
  • HIPAA compliance: signed forms never change structure

Custom Field Versions Table

  • Append-only (never updated or deleted)
  • Each version is a permanent historical record
  • Enables audit trails and compliance reviews

System Fields & Versioning

System fields (those with system_key set) are versioned the same way:

sql
custom_fields
  - id: 5
  - system_key: "patient_birthdate"  ← Immutable
  - key: "data-nasterii"             ← Can change (localization)
  - label: "Data nașterii"           ← Can change
  - version: 2

custom_field_versions
  - version: 1 → {system_key: "patient_birthdate", key: "birthdate", label: "Birthdate"}
  - version: 2 → {system_key: "patient_birthdate", key: "data-nasterii", label: "Data nașterii"}

Key point: system_key never changes across versions (stable identifier for PDF templates).

Performance Considerations

Snapshot Size

Each form instance stores a full field snapshot. For a form with 50 fields:

  • 50 field definitions × ~200 bytes = ~10KB per form
  • 10,000 forms × 10KB = ~100MB

This is acceptable for the immutability guarantee.

Query Optimization

sql
-- Efficient: Query current published version
SELECT * FROM custom_fields WHERE id = 42 AND published = true

-- Efficient: Query specific version
SELECT * FROM custom_field_versions WHERE custom_field_id = 42 AND version = 1

-- Index needed
CREATE INDEX idx_custom_field_versions_lookup
ON custom_field_versions(custom_field_id, version)

Migration Notes

From old architecture (no versioning):

  1. All existing custom fields become v1
  2. Archive current state to custom_field_versions
  3. Set published = true, version = 1
  4. Existing form templates continue referencing custom_field_id
  5. New forms automatically use v1 snapshot

No data loss - historical forms preserve their structure (they already had field snapshots in forms.fields JSONB).