The dynamic form engine in the Init-Website application provides a normalized, schema-driven framework for creating, editing, rendering, and submitting custom forms. The subsystem is engineered around a clean separation of concerns across database storage, administrative builder tools, and public rendering interfaces.
graph TD
subgraph Frontend Client
FB[FormBuilder Page] --> |Edits| FE[Form Builder Components]
FE --> |Debounced State| AE[Autosave Engine]
AE --> |RPC: save_form_definition| DB[(Supabase PostgreSQL)]
FR[FormRenderer Component] --> |Loads via RPC| publicRPC[get_public_form_definition]
FR --> |Validates| VE[formUtils: validateAnswers]
FR --> |Submits Response| DB
end
subgraph Supabase Database
DB --> |Table| Forms[public.forms]
DB --> |Table| FormItems[public.form_items]
DB --> |Table| FormItemOptions[public.form_item_options]
DB --> |Table| FormResponses[public.form_responses]
publicRPC --> |Aggregates| Forms
publicRPC --> |Aggregates| FormItems
publicRPC --> |Aggregates| FormItemOptions
end
- Relational Normalization: Form structure is decomposed into relational tables (
forms,form_items,form_item_options) rather than unindexed, monolithic JSON blobs. This enables index-backed constraints, efficient subqueries, and clean data integrity. - Optimistic Concurrency Control: Form updates rely on revision numbers (
revision) to prevent stale admin sessions from overwriting concurrent modifications. - Atomic Operations via Stored Procedures: Multi-table updates (deleting legacy fields/options and re-inserting updated structures) are executed inside atomic PL/pgSQL database functions.
- Non-Blocking Autosave Engine: The form editor maintains live state, executing background RPC saves with concurrency locking (
saveInFlightRef), edit queuing (dirtyDuringSaveRef), and request timeouts (AbortController). - Accessible Custom Form Components: Form rendering replaces default browser controls with styled Obsidian-themed interactive components while enforcing client-side validation and automatic scroll-to-error navigation.
The dynamic form subsystem is built on four core tables in the public schema.
erDiagram
forms ||--o{ form_items : "has items"
forms ||--o{ form_responses : "receives responses"
form_items ||--o{ form_item_options : "has options"
forms {
uuid id PK
text slug UK
text title
text description
text status
jsonb fields
jsonb settings
uuid created_by FK
integer revision
timestamp updated_at
timestamp created_at
}
form_items {
uuid form_id PK, FK
text item_id PK
text kind
text title
text description
boolean required
integer position
jsonb config
timestamp updated_at
timestamp created_at
}
form_item_options {
uuid form_id PK, FK
text item_id PK, FK
text option_id PK
text label
integer position
timestamp created_at
}
form_responses {
uuid id PK
uuid form_id FK
jsonb answers
jsonb respondent
jsonb metadata
timestamp submitted_at
}
Acts as the root container for a form definition.
| Column | Type | Constraints | Description |
|---|---|---|---|
id |
uuid |
PRIMARY KEY, Default gen_random_uuid() |
Unique identifier for the form. |
slug |
text |
NOT NULL, UNIQUE |
Unique URL-friendly slug used for public routes. |
title |
text |
NOT NULL |
Display title of the form. |
description |
text |
Optional | Detailed summary or header text for respondents. |
status |
text |
Default 'draft', Check ('draft', 'published', 'closed') |
Publication lifecycle state. |
fields |
jsonb |
Default '[]'::jsonb, NOT NULL |
Legacy field storage (cleared to [] after normalization). |
settings |
jsonb |
Default '{}'::jsonb, NOT NULL |
Global configuration options (auth requirements, schedule, limits). |
created_by |
uuid |
Foreign Key -> public.users(id) ON DELETE SET NULL |
Profile ID of the creator. |
revision |
integer |
Default 1, NOT NULL |
Monotonically increasing revision counter for optimistic lock check. |
created_at |
timestamptz |
Default now() |
Timestamp when record was created. |
updated_at |
timestamptz |
Default now() |
Timestamp when record was last updated. |
Stores discrete input questions or section dividers belonging to a form.
| Column | Type | Constraints | Description |
|---|---|---|---|
form_id |
uuid |
Foreign Key -> public.forms(id) ON DELETE CASCADE |
Parent form identifier. |
item_id |
text |
NOT NULL |
Unique item identifier within the form scope. |
kind |
text |
NOT NULL, Check ('text', 'email', 'number', 'textarea', 'select', 'radio', 'multiselect', 'checkbox', 'date', 'rating', 'section') |
Input control type or section header. |
title |
text |
NOT NULL |
Label or prompt displayed for the field. |
description |
text |
Optional | Help text or guidance hint displayed beneath label. |
required |
boolean |
Default false, NOT NULL |
Mandatory completion flag. |
position |
integer |
NOT NULL, Check (position >= 0) |
Display sequence index within canvas. |
config |
jsonb |
Default '{}'::jsonb, NOT NULL |
Structured metadata (placeholder, scale, validation). |
created_at |
timestamptz |
Default now() |
Creation timestamp. |
updated_at |
timestamptz |
Default now() |
Last modification timestamp. |
Primary Key & Indexes:
- Primary Key:
(form_id, item_id) - Unique Index:
idx_form_items_form_position ON (form_id, position) - Index:
idx_form_items_form_kind ON (form_id, kind)
Normalizes choice items for selectable fields (select, radio, multiselect).
| Column | Type | Constraints | Description |
|---|---|---|---|
form_id |
uuid |
NOT NULL |
Parent form identifier. |
item_id |
text |
NOT NULL |
Associated item identifier. |
option_id |
text |
NOT NULL |
Unique option choice identifier within item scope. |
label |
text |
NOT NULL |
User-visible text choice. |
position |
integer |
NOT NULL, Check (position >= 0) |
Display sequence index. |
created_at |
timestamptz |
Default now() |
Creation timestamp. |
Primary Key & Constraints:
- Primary Key:
(form_id, item_id, option_id) - Foreign Key:
(form_id, item_id) REFERENCES public.form_items(form_id, item_id) ON DELETE CASCADE - Unique Index:
idx_form_item_options_item_position ON (form_id, item_id, position)
Stores submitted responses from respondents.
| Column | Type | Constraints | Description |
|---|---|---|---|
id |
uuid |
PRIMARY KEY, Default gen_random_uuid() |
Response submission unique ID. |
form_id |
uuid |
Foreign Key -> public.forms(id) ON DELETE CASCADE |
Associated form ID. |
answers |
jsonb |
Default '{}'::jsonb, NOT NULL |
Key-value map (item_id -> submitted answer value). |
respondent |
jsonb |
Default '{}'::jsonb |
User identity details (user ID, email if authenticated). |
metadata |
jsonb |
Default '{}'::jsonb |
System submission metadata (IP, user agent, session duration). |
submitted_at |
timestamptz |
Default now() |
Submission timestamp. |
All read and write operations against form definitions are executed through security definer PL/pgSQL routines.
Handles atomic creation or update of forms, enforcing optimistic revision locking and replacing child items and options.
CREATE OR REPLACE FUNCTION "public"."save_form_definition"(
"p_form_id" uuid DEFAULT NULL,
"p_title" text DEFAULT NULL,
"p_description" text DEFAULT NULL,
"p_slug" text DEFAULT NULL,
"p_status" text DEFAULT 'draft',
"p_settings" jsonb DEFAULT '{}'::jsonb,
"p_created_by" uuid DEFAULT NULL,
"p_items" jsonb DEFAULT '[]'::jsonb,
"p_expected_revision" integer DEFAULT NULL
)
RETURNS TABLE (
"id" uuid,
"revision" integer
)- Authorization Verification: Evaluates
public.is_admin(). Raises error42501if user lacks admin credentials. - Validation: Enforces non-empty strings for
p_titleandp_slug. - Creation Path (
p_form_id IS NULL):- Inserts record into
public.formswithrevision = 1. - Returns generated
v_form_idand initial revision1.
- Inserts record into
- Update Path (
p_form_id IS NOT NULL):- Executes
UPDATE public.formssetting title, slug, description, status, settings, updatingupdated_at = now(), and incrementingrevision = forms.revision + 1. - Enforces optimistic lock:
WHERE forms.id = p_form_id AND (p_expected_revision IS NULL OR forms.revision = p_expected_revision). - If no row is modified (due to revision mismatch), raises error
40001(Form revision conflict). - Clears existing child items:
DELETE FROM public.form_items WHERE form_id = v_form_id(cascades to options).
- Executes
- Item & Option Reconstruction:
- Unpacks
p_itemsJSONB array usingjsonb_array_elements(...) WITH ORDINALITY. - Inserts rows into
public.form_items. - Unpacks options for each item using
CROSS JOIN LATERAL jsonb_array_elements_text(...) WITH ORDINALITY. - Inserts rows into
public.form_item_options.
- Unpacks
- Return Output: Returns table containing
idand incrementedrevision.
Constructs full form definition payload for administrative editing.
CREATE OR REPLACE FUNCTION "public"."get_form_definition"("p_form_id" uuid)
RETURNS jsonb- Verifies
public.is_admin(). Raises42501if non-admin. - Selects form record from
public.forms. - Executes subquery against
public.form_itemsordered byposition. - For each item, executes nested subquery against
public.form_item_optionsordered bypositionifkindis choice-based (select,radio,multiselect). - Uses
jsonb_build_objectandjsonb_strip_nullsto shape data matching frontendFormTypeScript interfaces. - Returns aggregated JSONB payload.
Retrieves published form structures for public respondents based on slug.
CREATE OR REPLACE FUNCTION "public"."get_public_form_definition"("p_slug" text)
RETURNS jsonb- Accessible to both
anonandauthenticatedroles. - Performs lookup matching
lower(trim(p_slug))and filtering bystatus = 'published'. - Aggregates items and options into identical JSON schema as
get_form_definition. - Returns NULL if form does not exist or is in
draft/closedstate.
Provides overview listing of all forms along with live response and item counts.
CREATE OR REPLACE FUNCTION "public"."list_forms_overview"()
RETURNS TABLE (
"id" uuid,
"slug" text,
"title" text,
"description" text,
"status" text,
"updated_at" timestamp with time zone,
"response_count" bigint,
"field_count" bigint,
"revision" integer
)- Verifies
public.is_admin(). - Selects fields from
public.forms. - Joins
LEFT JOIN LATERALsubquery computingCOUNT(*)frompublic.form_responses. - Joins
LEFT JOIN LATERALsubquery computingCOUNT(*)frompublic.form_itemsexcludingkind = 'section'. - Orders results by
updated_at DESC.
Located at src/components/forms/builder/ and driven by the page orchestrator src/pages/admin/FormBuilder.tsx.
graph LR
subgraph FormBuilder Canvas Layout
FP[FieldPalette] --> |onAddField| BC[BuilderCanvas]
BC --> |renders list| FC[FieldCard Elements]
FC --> |onSelect| FE[FieldEditor Drawer]
FSM[FormSettingsModal] -.-> |config| FormBuilder
FPM[FormPreviewModal] -.-> |simulates| FormRenderer
end
Sidebar component rendering element buttons for 11 field primitives:
- Short Text (
text) - Email (
email) - Number (
number) - Long Text (
textarea) - Dropdown (
select) - Radio Choice (
radio) - Multi-Select (
multiselect) - Checkbox (
checkbox) - Date Pick (
date) - Star Rating (
rating) - Section Divider (
section)
Central workspace displaying ordered form fields.
- Handles element reordering via
moveField(index, direction). - Renders empty state graphics when no fields are present.
- Maps
fieldsstate toFieldCardcomponents.
Individual card container for canvas fields.
- Visual icon indicator based on field
kind. - Reorder action buttons (
ChevronUp,ChevronDown). - Displays mandatory badges (
*), title, and help text hint. - Hover quick action bar for selection, duplication, and deletion.
Right-hand settings inspector drawer.
- Updates field label, placeholder, and help text.
- Toggles mandatory constraint (
required). - Configures star rating scales (5 vs 10 stars).
- Dynamic list manager for choice options (
select,radio,multiselect). - Configures validation rules (
min,max,minLength,maxLength, regexpattern).
Modal dialog for configuring global form behavior:
- Multi-submission permission (
allow_multiple_responses). - Authentication requirement (
require_auth). - Scheduling boundaries (
open_at,close_at). - Completion message (
success_message) and redirect target (redirect_url). - Maximum response limits (
max_responses). - Completion progress bar visibility (
show_progress_bar).
Full-screen modal offering live preview of form layout, replicating respondent perspective with disabled input fields.
The form editor uses a debounced autosave architecture in src/pages/admin/FormBuilder.tsx to persist updates without manual intervention.
sequenceDiagram
participant User
participant State as React State (FormBuilder)
participant Timer as Autosave Timer (1200ms)
participant Lock as saveInFlightRef Lock
participant RPC as Supabase RPC (save_form_definition)
User->>State: Edits field or form title
State->>Timer: Schedules debounced save (1200ms)
Note over Timer: Timer Expires
Timer->>Lock: Check saveInFlightRef
alt Lock is Free (false)
Lock->>Lock: Set saveInFlightRef = true
Timer->>RPC: Call save_form_definition(..., expected_revision)
RPC-->>State: Returns { id, revision: nextRev }
State->>State: Update revision = nextRev
Lock->>Lock: Set saveInFlightRef = false
else Lock is Busy (true)
Timer->>Lock: Set dirtyDuringSaveRef = true
Note over Lock: Active RPC completes
Lock->>RPC: Trigger queued save recursively
end
- Debounce Delay (1200ms): React
useEffectwatches dependencies ([title, description, slug, status, fields, settings]) and resetsautosaveTimerRefon changes. - First-Load Protection (
skipNextAutosaveRef): Flag set totrueduring initial RPC fetch or creation transition to prevent firing autosave on unmodified loaded state. - Concurrency Locking (
saveInFlightRef): Boolean ref preventing concurrent overlapping save requests. If an edit occurs while a save is in flight,dirtyDuringSaveRef.currentis flagged. - Edit Queue (
dirtyDuringSaveRef): When a save completes infinally, it checksdirtyDuringSaveRef. Iftrue, it immediately triggers a follow-up silent save to ensure recent changes are saved. - Timeout Protection (
AbortController&SAVE_TIMEOUT_MS): Enforces a 25-second maximum timeout (SAVE_TIMEOUT_MS = 25000) viaAbortController. Cancels hung network requests. - Unmount Cleanup:
useEffectcleanup handler clears pending timeouts and aborts active HTTP requests on component unmount. - Revision Synchronization: Updates local
revisionstate upon receiving response fromsave_form_definition, preserving the optimistic locking chain.
Located at src/components/forms/renderer/ and driven by FormRenderer.tsx.
Calculates and renders completion progress.
- Computes
percentage = Math.round((filled / total) * 100). - Ignores
sectionfields in calculation. - Renders animated gradient bar (
from-cyan-400 to-purple-500).
CustomFormSelect: Replaces browser native select elements with custom dark menu container, click-outside ref listener, active choice checkmarks, and smooth opening animations.- Rating Scale Buttons: Grid of numbered buttons (1 to scale limit) with cyan glow highlights on active state.
- Radio & Multi-Select Pills: Custom styled interactive selection pills with customized radio dot and checkbox checkmark icons.
- Date Inputs: Custom styled dark calendar input with calendar icon overlay.
Validation is driven by validateAnswers(fields, answers). Returns a record mapping fieldId -> error string | null.
export function validateAnswers(
fields: FormField[],
answers: Record<string, any>
): Record<string, string | null>- Required Fields: Checks if value is
undefined,null, empty string, or empty array. - Email Format: Evaluates regex
/^[^\s@]+@[^\s@]+\.[^\s@]+$/. - Numeric Rules: Validates
isNaN(num)and enforcesvalidation.minandvalidation.max. - Text Rules: Strips text and enforces
validation.minLength,validation.maxLength, and custom regexvalidation.pattern. - Multi-Select Rules: Verifies non-empty array selection for required multi-select fields.
- Live Validation Mode: After an initial submit attempt (
hasTriedSubmit = true), input value modifications trigger immediate live re-validation. - Error Card Highlights: Invalid fields render with red tinted borders (
border-red-500/25), red callout background, and an inline error badge with message. - Global Callout Alert: A persistent red alert banner appears at the top of the form when submission errors exist.
- Scroll-To-Error Behavior: On failed submission, the renderer locates the first invalid field ID and invokes:
document.getElementById(`el-${firstErrorId}`)?.scrollIntoView({
behavior: 'smooth',
block: 'center'
});Handled by helper utilities in src/utils/formDefinition.ts.
Converts in-memory FormField[] array into normalized SerializedFormItemInput[] structure expected by save_form_definition RPC:
- Orders fields by
position. - Strips null/empty validation properties using
compactValidation. - Formats configuration payload containing
placeholder,scale, andvalidation. - Converts array of string options into trimmed string arrays.
Parses database RPC output into frontend React state:
- Sorts
fieldsarray byorder. - Merges raw
settingsobject withdefaultSettingsfallbacks. - Handles revision assignment and null values safely.