Forms
Structured data collection — intake questionnaires, consent forms, assessments, and more — all built from a shared field library with complete immutability once signed.
What this enables
- Clinics design reusable form templates using a shared field library — define a field once, use it across any form
- When a patient fills in a field they've answered before (city, blood type, etc.), it's pre-filled automatically from their profile
- Forms attached to appointments are generated automatically — no manual setup per booking
- Once a patient signs a form, it becomes legally immutable — no one can alter what was recorded
- Consent forms automatically create a timestamped legal consent record when signed
- Specialists can have private fields that are visible during the session but excluded from patient-facing documents
How it works
Forms have two layers:
Templates (design time) — An admin builds a form by selecting fields from the field library, setting their order, and marking some as required or specialist-only.
Instances (runtime) — When an appointment is created, the system generates a form instance from the template. The instance captures a frozen snapshot of every field's definition at that exact moment — so even if the field definition changes later, the historical record is preserved exactly as it was.
Admin designs template (references field library)
↓
Appointment booked → form instance created from template
→ fields snapshot taken at current versions
→ patient's existing profile values auto-filled
↓
Patient fills form (in_progress)
→ saving a field also updates their profile (auto-fill loop)
↓
Patient submits (completed)
↓
Patient signs (signed) — IMMUTABLE from this point
→ if it's a consent form, consent record created automaticallyConsent forms
When a form with type='disclaimer' is signed, the platform automatically creates a user_consents entry with:
- The consent type (e.g.,
hipaa_notice,video_recording) - Timestamp and IP address
- A link back to the form that was signed
This creates a full legal audit trail without any manual step.
Private fields
Specialists often need to record clinical observations that shouldn't appear in the patient's copy of the document. Fields marked private: true are:
- Visible to both patient and specialist during form filling
- Excluded from patient-facing PDFs and documents
- Always visible to admin and specialist exports
Technical Reference
Everything below is intended for developers.
Architecture
Templates reference custom fields by ID — they don't duplicate field schemas. This is the key architectural decision:
- Field changes (new options, updated label) propagate automatically to all templates
- Form instances snapshot the field at the current version at creation time — immutability preserved
- One-off fields (
custom_field_id: null) are supported for form-specific fields that shouldn't sync to profile
Template structure
{
"id": 42,
"title": "Patient Intake Survey",
"type": "survey",
"category": "first_appointment",
"pdf_template_id": 5,
"consent_types": [],
"version": 1,
"published": true,
"fields": [
{ "custom_field_id": 10, "sort_order": 1, "required": true },
{ "custom_field_id": 11, "sort_order": 2, "private": true },
{
"custom_field_id": null,
"key": "chief_complaint",
"type": "textarea",
"label": "What brings you in today?",
"sort_order": 3,
"required": true
}
]
}Template types & categories
| Type | Use Case |
|---|---|
disclaimer | Consent forms — signing auto-creates user_consents |
survey | Patient questionnaires, feedback |
parameters | Clinical measurements |
report | Assessment reports |
advice | Care recommendations |
prescription | Medication prescriptions |
| Category | When generated |
|---|---|
new_patient | First-time registration |
first_appointment | First consultation |
new_appointment | Recurring appointments |
Template versioning
On publish, a new version is created. Existing form instances are unaffected — they hold a snapshot of the version at creation time. New form instances use the latest published version.
Form instance lifecycle
pending → in_progress → completed → signed (immutable)pending→in_progress: first value savedin_progress→completed: all required fields filledcompleted→signed: explicit patient confirmation- After
signed: values, fields, and files cannot be changed (API returns 409)
Auto-fill flow
- Form instance created from template
- For each field with a
custom_field_id, backend looks up existingcustom_field_valuesfor the patient - Found values are pre-filled in
form.values - When patient saves values,
custom_field_valuesare upserted for fields with acustom_field_id - Next form with the same field auto-fills from the updated profile
One-off fields (custom_field_id: null) do NOT sync to profile — they're appointment-specific.
Instance snapshot structure
{
"fields": [
{
"custom_field_id": 10,
"version": 2,
"key": "city",
"field_type": "text",
"required": true
}
],
"values": {
"field_10": "Amsterdam"
},
"files": {
"consent_signature": {
"s3_key": "org-1/forms/1024/signature/abc.png",
"size": 45000
}
}
}Multi-user filling
Both patient and specialist can write to the same form instance. Last write wins (JSONB merge). Each save is recorded in the audit log with the acting user_id. Field-level authorship tracking is deferred to v2.
Files in forms
File fields (signatures, attachments) are uploaded to S3. Access via signed URLs with 15-minute expiry.
Data storage
All form values are stored as plaintext JSONB (not app-level encrypted):
- Infrastructure encryption (AWS RDS AES-256) satisfies HIPAA at-rest requirement
- Fully queryable for segments and analytics
- The
privateflag controls document visibility, not encryption
API overview
POST /v1/form-templates Create template (draft)
GET /v1/form-templates List templates
PATCH /v1/form-templates/{id} Edit template (sets to draft)
POST /v1/form-templates/{id}/publish Publish (increments version)
GET /v1/form-templates/{id}/versions Version history
POST /v1/forms Create form instance (usually automatic)
GET /v1/forms/{id} Get form
PATCH /v1/forms/{id} Update values (fails if signed)
POST /v1/forms/{id}/sign Sign (immutable from here)
POST /v1/forms/{id}/files Upload file to form field
GET /v1/forms/{id}/files/{key} Get signed URL for fileAudit
All mutations (form.create, form.update, form.sign) are logged with resource_type = "form". Field names are logged — never field values — to ensure no PHI in the audit trail.