Explanation: Dynamic Forms Engine Design
Architectural explanation of JSONB schemas, dynamic runtime Zod validation, and local draft persistence in the forms engine
The Dynamic Forms Engine is the core component that allows restaurant managers to design, deploy, and analyze location-specific checklists (e.g., Temperature Logs, Opening Checklists, Incident Reports). This document explains the architecture of the engine, the selection of PostgreSQL JSONB schemas, dynamic runtime Zod validation, and stateful form autosaving.
Why JSONB over Normalized Relational Tables?
Checks and checklists in restaurants evolve rapidly. A manager might add a "Walk-in Temp" number field today, modify it to a checkbox tomorrow, or attach location-specific parameters.
Implementing this under a standard normalized relational model would require:
- Creating a
form_fieldstable,form_field_optionstable, and an EAV (Entity-Attribute-Value) model for submissions. - High query complexity (multiple joins) to render a single form or extract submissions.
- Complex migrations when structural properties of field validation parameters change.
JSONB Schema Architecture
Instead, forms and their dynamic field definitions are stored as JSONB directly in the forms table.
CREATE TABLE forms (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
fields JSONB NOT NULL, -- Array of FormField schema definitions
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);This structural decision provides:
- Zero Migration Overhead: Modifying, adding, or deleting fields is a simple JSON write. No database schema changes are required.
- Schema Versioning: Submissions store the snapshot of the form schema at the moment of submission, guaranteeing that modifying a form definition does not break historical reporting.
Dynamic Zod Schema Generator
Since form fields are dynamic, we cannot compile a static TypeScript schema for validation. Instead, the form renderer parses the JSONB field definitions and constructs a Zod Schema at Runtime.
The parser maps each JSON field definition to its corresponding Zod validation primitive:
// apps/app/src/app/(authenticated)/forms/[formId]/form-submission-client.tsx
import { z } from "zod";
export function generateZodSchema(fields: FormField[]) {
const schemaShape: Record<string, z.ZodTypeAny> = {};
for (const field of fields) {
let fieldSchema: z.ZodTypeAny;
switch (field.type) {
case "number":
fieldSchema = z.number({
invalid_type_error: `${field.label} must be a number`,
});
if (field.validation?.min !== undefined) {
fieldSchema = (fieldSchema as z.ZodNumber).min(field.validation.min);
}
if (field.validation?.max !== undefined) {
fieldSchema = (fieldSchema as z.ZodNumber).max(field.validation.max);
}
break;
case "multiselect":
fieldSchema = z.array(z.string()).min(field.required ? 1 : 0, {
message: `Select at least one option for ${field.label}`,
});
break;
case "checkbox":
fieldSchema = z.boolean();
break;
case "date":
fieldSchema = z.string().datetime({
message: `${field.label} must be a valid date`,
});
break;
case "email":
fieldSchema = z.string().email({
message: `${field.label} must be a valid email`,
});
break;
default:
// text and textarea
fieldSchema = z.string();
if (field.required) {
fieldSchema = (fieldSchema as z.ZodString).min(1, {
message: `${field.label} is required`,
});
}
if (field.validation?.max) {
fieldSchema = (fieldSchema as z.ZodString).max(field.validation.max);
}
}
if (!field.required && field.type !== "checkbox" && field.type !== "multiselect") {
fieldSchema = fieldSchema.optional().nullable();
}
schemaShape[field.id] = fieldSchema;
}
return z.object(schemaShape);
}This dynamically constructed schema is passed directly into React Hook Form via the @hookform/resolvers/zod resolver.
State Lifecycle & autosave
Dynamic forms integrate React Hook Form state with the browser's localStorage to handle network disruptions or accidental closures common in busy restaurant environments.
graph TD
A[User Opens Form] --> B(Draft in localStorage?)
B -- Yes --> C[Restore Form State]
B -- No --> D[Empty Form State]
C --> E[User Types / Interacts]
D --> E
E --> F[Throttle Auto-save: 30s]
F --> G[Save to localStorage]
E --> H[Submit Button Clicked]
H --> I[Zod Validation Run]
I -- Valid --> J[Execute Server Action]
J --> K[Clear localStorage Draft]
J --> L[Redirect to List]
I -- Invalid --> M[Render Form Errors]Attachments
Dynamic forms do not support photo or file uploads. Use Shift Reports or Incidents for attachments.
Explanation: AI Integration & Tenant Security
Architectural explanation of the Vercel AI Gateway tool-calling agent, context window packing, and tenant isolation boundaries
Explanation: Compliance Ledger Design
Architectural explanation of the decoupled compliance ledger, service week boundaries, and dual filing obligations