# How to Add a Dynamic Form Builder to Your App (Copy-Paste Prompts Included)

- Tool: App Features
- Last updated: July 2026

## TL;DR

A dynamic form builder stores form definitions as JSONB in Supabase, renders them with react-hook-form, and collects submissions in a separate table. With V0 or Lovable you can ship a working builder in 3–5 days for $0–$25/month. The hard parts are drag-and-drop field reordering (@dnd-kit), conditional logic evaluation, and anonymous submission RLS — not the form rendering itself.

## What a Dynamic Form Builder Actually Is

A dynamic form builder lets your users — not just you — create forms. The form definition is stored as a JSON schema in Supabase, a separate renderer reads that schema and draws the live form, and submissions land in their own table. The builder itself is a drag-and-drop canvas where users add fields (text, dropdown, file upload, date, rating, signature), configure validation, and set conditional logic rules like 'show Question 3 only if Question 2 equals Yes.' The product decisions that matter: single-page versus multi-page wizard, whether forms are publicly submittable or authenticated-only, and whether conditional logic ships at launch or in phase 2. Those choices dictate complexity more than the form rendering itself.

## Anatomy of the Feature

Six components. The form renderer and submission collector are the easiest parts. Drag-and-drop and conditional logic are where AI-generated builds most often fail on the first attempt.

- **Form schema editor** (ui): A drag-and-drop canvas built with @dnd-kit/core and @dnd-kit/sortable that renders a live list of field definitions. Each field has a type, label, placeholder, validation rules (required, min/max length, regex), and a conditional logic config. The entire form definition is stored as a JSONB object in the Supabase `forms` table — no separate columns per field type.
- **Field type registry** (ui): A library of field components each implementing a common interface. Text and textarea use react-hook-form register(). Select and multi-checkbox use Radix UI Select and Radix UI Checkbox. Date uses react-day-picker. Star rating is a custom SVG component. File upload triggers a Supabase Storage presigned URL. Signature pad uses the signature_pad npm package. Each renders inside the form renderer using a switch on field.type.
- **Conditional logic engine** (backend): A client-side JSON rule evaluator that reads a `conditions` array on each field — each condition is `{ field_id, operator, value }` — and evaluates whether to show or hide the field on every form value change. Operators: equals, not_equals, contains, gt, lt. The evaluator is a pure JavaScript function, so no server round-trip is needed for show/hide toggling. Rules are stored inside the field definition JSONB in Supabase.
- **Form renderer** (ui): A separate read-only component that consumes a form schema JSON and renders the live form using react-hook-form for validation. Handles single-page layout and multi-page wizard layout (if the schema includes a `pages` array). Validates required fields and zod rules client-side before submission. Shows inline error messages per field on blur.
- **Submission collector** (backend): A Supabase Edge Function (Deno) that receives a POST with form_id and an answers object, validates required fields server-side against the stored schema, writes a row to `form_submissions`, and optionally fires a webhook to n8n or Zapier if a webhook URL is configured in the form's settings JSONB. Returns a submission_id for the confirmation screen.
- **Submissions dashboard** (data): A Supabase query on `form_submissions` filtered by form_id, rendered with TanStack Table v8 (react-table). Each row displays submitted_at and the answers JSONB expanded into columns. Supports column-level filtering and search. Exports to CSV via papaparse (Papa.unparse()) triggered by a Download button.

## Data model

Two tables: `forms` stores schema definitions owned by authenticated users; `form_submissions` accepts anonymous inserts for published forms. Run this in the Supabase SQL editor.

```sql
create table public.forms (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users(id) on delete cascade not null,
  title text not null,
  schema jsonb not null default '{"fields": [], "pages": null}'::jsonb,
  settings jsonb not null default '{"is_password_protected": false, "webhook_url": null, "allow_multiple_submissions": true}'::jsonb,
  is_published boolean not null default false,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  constraint schema_has_fields check (schema ? 'fields')
);

create table public.form_submissions (
  id uuid primary key default gen_random_uuid(),
  form_id uuid references public.forms(id) on delete cascade not null,
  answers jsonb not null default '{}'::jsonb,
  submitted_at timestamptz not null default now(),
  ip_hash text,
  metadata jsonb default '{}'::jsonb
);

alter table public.forms enable row level security;
alter table public.form_submissions enable row level security;

-- Form owners can do everything with their own forms
create policy "Owners manage own forms"
  on public.forms for all
  using (auth.uid() = user_id)
  with check (auth.uid() = user_id);

-- Anyone can submit to a published form (anonymous allowed)
create policy "Anyone can submit to published forms"
  on public.form_submissions for insert
  with check (
    form_id in (
      select id from public.forms where is_published = true
    )
  );

-- Only form owners can read submissions for their forms
create policy "Owners read own form submissions"
  on public.form_submissions for select
  using (
    form_id in (
      select id from public.forms where user_id = auth.uid()
    )
  );

create index forms_user_idx on public.forms (user_id, created_at desc);
create index submissions_form_idx on public.form_submissions (form_id, submitted_at desc);
```

The CHECK constraint on `forms.schema` guards against completely malformed inserts, but full JSONB field-level validation should happen client-side with a zod schema before the Supabase upsert. The submissions index keeps the dashboard fast once a popular form accumulates thousands of rows.

## Build paths

### Lovable — fit 3/10, 3-5 days

Lovable scaffolds the Supabase schema and form renderer quickly, but drag-and-drop wiring and conditional logic often need multiple correction prompts and can trigger credit-burning loop cycles.

1. Create a new Lovable project, connect Lovable Cloud so Supabase is auto-provisioned, then open the Cloud tab and verify the `forms` and `form_submissions` tables appear after the first prompt
2. Paste the prompt below in Agent Mode — Lovable will build the schema editor canvas, the field type components, the renderer, and the submissions dashboard in one pass
3. If drag-and-drop is broken after the first build (fields not reorderable), add a follow-up prompt: 'Fix the @dnd-kit DndContext — it must wrap the entire SortableContext without extra divs between them; draggable items need useSortable() from @dnd-kit/sortable'
4. Test conditional logic by creating a form with a Yes/No dropdown and a field set to appear only when the answer is Yes; verify the field shows and hides in the live preview panel
5. Test anonymous form submission by opening the public form URL in an incognito window; verify the row appears in the submissions dashboard
6. Click Publish and share the form URL with a real user to confirm the full submission flow end-to-end

Starter prompt:

```
Build a dynamic form builder feature with Supabase. Schema: `forms` table (id, user_id, title, schema JSONB with a `fields` array and optional `pages` array, settings JSONB, is_published BOOLEAN, created_at). `form_submissions` table (id, form_id, answers JSONB, submitted_at, ip_hash, metadata JSONB). RLS: forms only accessible by owner; form_submissions insertable by anyone when the parent form is_published=true, readable only by the form owner. Builder canvas: drag-and-drop field reordering using @dnd-kit/core and @dnd-kit/sortable. Field types: text input, textarea, single select dropdown (Radix UI), multi-checkbox (Radix UI), date picker (react-day-picker), star rating (1-5 stars, custom SVG), file upload (Supabase Storage), signature pad (signature_pad package). Each field has: label, placeholder, required toggle, help text, and a conditional logic section (show this field only if [other field] [equals/contains/gt/lt] [value]). Evaluate conditions client-side using react-hook-form watch() — no server round-trip. Right panel: field config sidebar that opens when a field is selected. Top bar: form title input, Preview toggle that switches to the live renderer, Publish toggle. Renderer: separate read-only component using react-hook-form with zod validation; validates required fields on submit. Submissions dashboard: TanStack Table v8 showing all submissions with answers expanded as columns; CSV export via papaparse. UI states: empty canvas with 'Add your first field' prompt, field config sidebar open, live preview mode, submission success screen with confetti, form unpublished warning.
```

Limitations:

- @dnd-kit wiring requires specific component nesting that Lovable sometimes gets wrong on the first pass — budget one follow-up prompt for the drag-and-drop fix
- Conditional logic JSONB schema needs careful prompt engineering; vague descriptions produce evaluators that mutate state and cause infinite re-renders
- File upload field preview may not work in the Lovable preview iframe due to sandboxing — test on the published URL
- Complex multi-page wizard layouts with branching logic (skip to page 3 if condition X) are likely to require significant rework after the initial AI build

### V0 — fit 4/10, 2-4 days

V0's Next.js + shadcn/ui foundation is the stronger choice for the builder UI layer — TanStack Table, react-hook-form, and @dnd-kit are well within its training data. Best when the form builder is part of a larger app.

1. Prompt V0 with the spec below; it will generate the builder canvas component, field registry, renderer, and submissions dashboard as separate Next.js components
2. Add Supabase environment variables in the Vars panel: NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY
3. Run the SQL schema from this page in the Supabase SQL editor to create the `forms` and `form_submissions` tables with RLS
4. Add @dnd-kit/core and @dnd-kit/sortable to the project dependencies via the V0 package panel or by prompting 'add @dnd-kit/core and @dnd-kit/sortable to dependencies'
5. Test the conditional logic by building a test form with a branching question in the V0 preview, then confirm the anonymous submission flow on the published URL

Starter prompt:

```
Build a dynamic form builder in Next.js with App Router and shadcn/ui. Use @dnd-kit/core and @dnd-kit/sortable for drag-and-drop field reordering on the builder canvas. Field types: text, textarea, select (shadcn Select), multi-checkbox (shadcn Checkbox), date (react-day-picker), star rating (1-5 custom SVG), file upload, signature (signature_pad). Each field definition is a JSONB object: { id, type, label, placeholder, required, helpText, conditions: [{ field_id, operator, value }] }. Store the full form schema in Supabase `forms.schema` as JSONB. Conditional logic: evaluate conditions client-side using react-hook-form watch() with useMemo; operators: equals, not_equals, contains, gt, lt. Renderer component: accepts a form schema prop and renders using react-hook-form with zod validation; show/hide fields based on evaluated conditions. Submissions: Next.js Server Action that validates required fields, inserts to Supabase `form_submissions` (answers JSONB, form_id, submitted_at, ip_hash). Dashboard page: TanStack Table v8 with dynamic columns from form schema; CSV export button using papaparse Papa.unparse(). Public form page at /forms/[id] renders the form for anonymous users (no auth required). UI states: empty canvas with add-field prompt, field sidebar on click, preview mode toggle, publish toggle, submission success with confirmation message, form not found 404 state.
```

Limitations:

- V0 does not auto-provision Supabase — you must run the SQL schema manually and add environment variables
- Server Actions add a round-trip that can slow real-time preview updates in the builder; for instant preview, the renderer should read from local React state rather than re-fetching from Supabase on every schema change
- Tailwind v3/v4 mismatch can break custom field component styles — check that your project's Tailwind version matches what V0 generated

### Flutterflow — fit 2/10, 4-6 days

FlutterFlow can build the form-filling side (user submitting a form on mobile), but building the form builder canvas itself — the drag-and-drop schema editor — is not supported visually and requires extensive custom Dart code.

1. In FlutterFlow, create a page that fetches a form schema from Supabase and uses a ListView to render form fields dynamically based on the schema type field
2. Add a custom Dart action to evaluate conditional logic rules from the schema JSONB — this cannot be done with FlutterFlow's visual action editor
3. Wire the submit button to a Supabase insert action writing the collected answers as a JSONB object to `form_submissions`
4. For the builder side (creating forms), accept that FlutterFlow requires full custom Dart widget code for the drag-and-drop canvas — consider building the builder in V0/Lovable for web and using FlutterFlow only for the mobile form-filler client

Limitations:

- No native drag-and-drop canvas widget in FlutterFlow — the form builder (schema editor) side is not buildable visually
- JSON schema manipulation and conditional logic evaluation require custom Dart code actions, adding significant complexity
- FlutterFlow Pro ($70/mo) required for code export if isolate or background thread customization is needed
- Practical only for the form-filling (submission) side of a mobile app, not the builder side

### Custom — fit 5/10, 3-4 weeks

The correct path for production-grade form builders: full React + @dnd-kit + react-hook-form + Supabase JSONB with schema versioning, team collaboration, and white-label embedding.

1. Build the form schema editor as a React component tree: DndContext > SortableContext > map(fields) > DraggableField; each DraggableField renders its config sidebar inline on click
2. Implement a JSONB schema versioning strategy so editing a published form doesn't break existing submission data — store a `schema_version` integer and migrate old submissions on read
3. Add real-time collaborative editing via Supabase Realtime channels so multiple team members see cursor positions and field changes live
4. Build the embeddable public form renderer as an isolated React component that can be loaded via a `<script>` tag on external sites with a custom CSS theming API

Limitations:

- Highest upfront investment — budget 3-4 weeks for a production-quality builder
- JSONB schema versioning requires careful migration strategy — breaking changes to the schema shape break both the renderer and the submissions dashboard

## Gotchas

- **JSONB schema not validated on insert causes silent renderer crashes** — Supabase stores any JSONB in `forms.schema` without type checking. A field definition missing the `type` key (or with a typo like 'text-area' instead of 'textarea') is saved successfully, then causes the form renderer to crash with a blank white screen when it hits the unknown type in its switch statement. The error appears for every user who opens the form. Fix: Add a zod schema on the client that validates the entire form schema object before saving to Supabase — including the shape of each field's conditions array. Also add a Postgres CHECK constraint: `check (schema ? 'fields')` to catch completely broken schemas at the database level. Show a validation error in the builder UI before the save ever reaches Supabase.
- **RLS blocks anonymous form submissions on public forms** — The default Supabase RLS policy pattern uses `auth.uid() = user_id` — but anonymous form submitters have no auth.uid(). If the INSERT policy on `form_submissions` requires authentication, every public form submission silently returns a 403 and the user sees no error message unless you handle the Supabase error explicitly. Fix: Add an INSERT policy that allows anonymous inserts only for published forms: `WITH CHECK (form_id IN (SELECT id FROM forms WHERE is_published = true))`. This keeps reads restricted to form owners while allowing public submissions. Test this specifically in an incognito window before launch.
- **Drag-and-drop stops working silently after an AI edit** — Lovable and V0 sometimes wrap @dnd-kit's DndContext with an extra div (e.g., a shadcn/ui Card or a layout container) that intercepts pointer events before they reach the drag listeners. Fields appear draggable (cursor changes) but releasing the mouse doesn't reorder them. Fix: Inspect the rendered DOM in browser DevTools and confirm DndContext directly wraps SortableContext with no elements between them that have pointer-events: none or overflow: hidden. Re-prompt specifying: 'DndContext must be the direct parent of SortableContext; no wrapper divs, Cards, or overflow-hidden containers between them.'
- **Conditional logic evaluator causes infinite re-render loop** — An evaluator that derives a new object from form values inside a useEffect with the derived object as its own dependency re-triggers the effect on every render. React detects the render loop and throws 'Too many re-renders' after several cycles. This commonly happens when AI tools compute a `visibleFields` array inside a useEffect and set it as state. Fix: Move the condition evaluator to a useMemo that takes the raw react-hook-form watch() values as its dependency: `const visibleFields = useMemo(() => fields.filter(f => evaluateConditions(f.conditions, watchValues)), [watchValues, fields])`. The memo re-computes only when actual form values change, not on every render.
- **File upload fields silently fail in the Lovable preview iframe** — Lovable's preview iframe sandbox blocks file input triggers in Chromium-based browsers. The upload button renders correctly and looks functional, but clicking it does nothing — no file picker opens. This affects both the builder preview and any form tested inside the iframe. Fix: File upload fields can only be tested on the published URL, not inside the Lovable preview. Add a note in the builder UI: 'File upload fields will work on the published form.' As a development workaround, replace file upload with a URL input field during initial testing, then swap back once you're testing on the live URL.

## Best practices

- Validate the entire form schema with zod before every Supabase upsert — never trust the builder canvas to produce valid JSON without a schema guard
- Implement duplicate submission prevention per form: store a submission fingerprint (hash of answers + IP + form_id) and reject duplicates within a 60-second window for public forms
- Decouple the renderer component completely from the builder — it should render from a schema prop alone so it can be unit-tested, embedded in iframes, and shared as a public form without importing builder dependencies
- Always evaluate conditional logic client-side using react-hook-form watch() — never fire a server request to show or hide a field; perceived latency kills form completion rates
- Store webhook URLs in the form's settings JSONB rather than hardcoding them — this lets form owners configure integrations without a code deploy
- Export submissions as CSV using papaparse with quotes: true — field values often contain commas from address inputs or multi-line notes that corrupt unquoted CSV files in Excel
- Add server-side required-field validation in the Edge Function even though the renderer also validates client-side — the submission endpoint can be called directly bypassing the form UI
- Show a character count on text fields with max-length constraints and a progress indicator on multi-page forms — these two changes measurably improve form completion rates

## Frequently asked questions

### Can I build a form builder where MY users create their own forms — not just me?

Yes. The key architectural shift is storing the form definition as JSONB owned by the user who created it, with a separate renderer that reads that JSON at runtime. Each user gets rows in the `forms` table scoped to their user_id, and the renderer is a stateless component that accepts any valid schema. This is exactly the pattern described in this guide — the builder UI writes to Supabase, the public form URL reads from it.

### How do I add conditional logic so questions appear only when relevant?

Store conditions as a JSON array on each field: `[{ field_id, operator, value }]`. On the renderer side, call react-hook-form's watch() to get live form values and evaluate each field's conditions with a pure JS function inside useMemo. The field renders or returns null based on the evaluation result. The critical detail: the evaluator must be memoized and take the raw watch() output as its dependency — any derived state between the evaluator and the dependency array triggers infinite re-render loops.

### Where are form submissions stored — do I need a database?

Yes, a database. The submission collector (a Supabase Edge Function) validates answers server-side and writes them to the `form_submissions` table as a JSONB answers object. Supabase's free tier handles thousands of submissions per month. The alternative — email-only submission without a database — works only if you never need to query, filter, or export submission data, which most form use cases require within weeks of launch.

### Can I embed forms on an external website — not just in my app?

Yes, but it requires the renderer to be built as a standalone embeddable component. The simplest approach: host the renderer at a public URL and embed it in an iframe on the external site. A more robust approach: publish the renderer as a JavaScript snippet that mounts to a div on the host page. The iframe approach is achievable with an AI build; the JavaScript snippet approach requires custom development to handle cross-origin communication, theming, and submission callbacks.

### How do I prevent spam submissions on a public form?

Three layers: first, add Cloudflare Turnstile (free) as a hidden CAPTCHA on the submission form — it blocks most bots without user friction. Second, rate-limit the submission Edge Function by IP using Upstash Ratelimit (free tier: 10K requests/day). Third, add a duplicate fingerprint check in the Edge Function that rejects submissions with identical answers from the same IP within 60 seconds. Together these prevent the vast majority of spam without requiring submitters to log in.

### Can users upload files through the form?

Yes. File upload fields upload to Supabase Storage via a presigned URL and store the file path in the submission's answers JSONB. Set file type restrictions in the react-dropzone accept prop (e.g., PDFs and images only) and size limits (typically 10MB for public forms). The file URL in the submission record lets form owners download attachments from the submissions dashboard.

### How do I export all responses to a spreadsheet?

Add a Download CSV button to the submissions dashboard that calls Papa.unparse() from the papaparse library on the submissions array fetched from Supabase. For Excel output, use ExcelJS in a Supabase Edge Function — it produces a proper .xlsx with bold headers and auto-sized columns. Always use `quotes: true` in Papa.unparse() to handle field values that contain commas or line breaks.

### What's the difference between building a form builder vs just using Typeform or Tally?

Typeform and Tally are right for standalone forms. Build your own when the form is embedded in a product your users own — meaning your users need to create and manage forms within your app under your branding, with submissions stored in your database alongside the rest of your data. The moment you need custom logic, white-label embedding, or submission data joined with your own tables for reporting, external tools become a dead end.

---

Source: https://www.rapidevelopers.com/app-features/dynamic-form-builder
© RapidDev — https://www.rapidevelopers.com/app-features/dynamic-form-builder
