2025
Form Builder
A from-scratch drag-and-drop form engine — authoring, schema storage and a runtime renderer.
Problem
Typeform and Google Forms stop at a single self-contained form. The product needed journeys — a form whose answers decide which form comes next, some updating the person filling them in, others creating linked records for people who aren't in the room yet. That relationship between forms is the actual requirement, and it isn't something a hosted form tool exposes.
My role
Sole engineer: the schema design, the drag-and-drop authoring UI, the runtime renderer and the submission pipeline.
Outcome
A pages → sections → fields authoring UI backed by dnd-kit, a zod schema persisted as a single JSON column, and a runtime renderer with a stepper, conditional visibility and resume-after-refresh.
The design decision everything else follows from: the zod schema is the model. Static types are inferred from it with z.infer rather than declared alongside it, so the shape used to validate a submission at runtime and the shape the editor compiles against cannot drift apart. One form is one record whose definition column holds the entire structure as JSON — no join tables for pages, sections or fields, because the whole definition is only ever read and written as a unit.
Storing the definition as a single opaque column is the trade. It buys atomic saves and a trivial versioning story; it costs the ability to query across forms in the database — you cannot ask 'which forms have a required email field' in SQL, only in application code. For a few hundred forms edited by admins that's the right side of the trade.
The trap worth recording is an organisational one rather than a technical one. In the reference implementation there is no shared package: the schema, the conditional-logic evaluator and the zod-schema compiler are copied byte-identically into the admin app and the portal app, and every change has to land in both. It works, and it is the single most reliable source of bugs in the system. In a fresh monorepo that code is a shared package on day one.