Skip to main content
RapidDev - Software Development Agency
App Featuresforms-data21 min read

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

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.

4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members

Feature spec

Advanced

Category

forms-data

Build with AI

3-5 days with Lovable or V0 + Supabase

Custom build

3-4 weeks custom dev

Running cost

$0-25/mo up to 1K users; $50-100/mo at 10K users

Works on

WebMobile

Everything it takes to ship a Dynamic Form Builder — parts, prompts, and real costs.

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.

What users consider table stakes in 2026

  • Drag-and-drop field reordering with a live preview that matches the final form exactly
  • Conditional logic rules per field: show/hide based on another field's value, with at least equals, contains, and greater-than operators
  • Multiple field types at launch: text, dropdown, multi-checkbox, file upload, date picker, star rating, and a signature pad
  • Shareable public form URL with optional password protection for gated forms
  • Submission dashboard showing all answers in a table with column filters and one-click CSV export
  • Mobile-responsive form renderer — the builder may be desktop-only, but submitted forms must work on any screen

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.

Layers:UIDataBackend

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.

Note: DndContext must wrap the entire sortable list; extra wrapper divs between DndContext and SortableContext break pointer event propagation and make fields un-draggable after an AI edit.

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.

Note: Signature_pad and react-day-picker need explicit version pinning — AI tools sometimes pick major-version mismatches that break their APIs silently.

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.

Note: Memoize the evaluator with useMemo and take field values from react-hook-form's watch() as the dependency. Deriving a new object inside the evaluator and using it as a useEffect dependency causes infinite re-renders.

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.

Note: Keep the renderer completely decoupled from the editor — it should work as a standalone embeddable component so the same code powers both the live preview panel and the public form URL.

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.

Note: Server-side validation is essential — the client-side renderer can be bypassed. Validate that form_id exists and is_published=true before accepting any submission.

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.

Note: The answers JSONB structure varies per form, so column headers must be derived dynamically from the form schema — not hardcoded.

The 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.

schema.sql
1create table public.forms (
2 id uuid primary key default gen_random_uuid(),
3 user_id uuid references auth.users(id) on delete cascade not null,
4 title text not null,
5 schema jsonb not null default '{"fields": [], "pages": null}'::jsonb,
6 settings jsonb not null default '{"is_password_protected": false, "webhook_url": null, "allow_multiple_submissions": true}'::jsonb,
7 is_published boolean not null default false,
8 created_at timestamptz not null default now(),
9 updated_at timestamptz not null default now(),
10 constraint schema_has_fields check (schema ? 'fields')
11);
12
13create table public.form_submissions (
14 id uuid primary key default gen_random_uuid(),
15 form_id uuid references public.forms(id) on delete cascade not null,
16 answers jsonb not null default '{}'::jsonb,
17 submitted_at timestamptz not null default now(),
18 ip_hash text,
19 metadata jsonb default '{}'::jsonb
20);
21
22alter table public.forms enable row level security;
23alter table public.form_submissions enable row level security;
24
25-- Form owners can do everything with their own forms
26create policy "Owners manage own forms"
27 on public.forms for all
28 using (auth.uid() = user_id)
29 with check (auth.uid() = user_id);
30
31-- Anyone can submit to a published form (anonymous allowed)
32create policy "Anyone can submit to published forms"
33 on public.form_submissions for insert
34 with check (
35 form_id in (
36 select id from public.forms where is_published = true
37 )
38 );
39
40-- Only form owners can read submissions for their forms
41create policy "Owners read own form submissions"
42 on public.form_submissions for select
43 using (
44 form_id in (
45 select id from public.forms where user_id = auth.uid()
46 )
47 );
48
49create index forms_user_idx on public.forms (user_id, created_at desc);
50create index submissions_form_idx on public.form_submissions (form_id, submitted_at desc);

Heads up: 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 it — pick your path

Each tool fits this feature differently. Switch tabs to compare fit, time, the exact steps, and a copy-paste prompt.

Hand-built by developersFit for this feature:

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.

Step by step

  1. 1Build the form schema editor as a React component tree: DndContext > SortableContext > map(fields) > DraggableField; each DraggableField renders its config sidebar inline on click
  2. 2Implement 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. 3Add real-time collaborative editing via Supabase Realtime channels so multiple team members see cursor positions and field changes live
  4. 4Build 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

Where this path bites

  • 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

Third-party services you'll need

The form builder core (drag-and-drop, rendering, submission storage) runs on free open-source libraries. You only pay for Supabase storage at scale and optionally for submission automation.

ServiceWhat it doesFree tierPaid from
SupabaseJSONB storage for form schemas, submission data, and Storage for file upload fields500MB DB, 1GB Storage, 2 projectsPro $25/mo (8GB DB, 100GB Storage)
papaparseCSV export for the submissions dashboard — converts JSONB answers to downloadable CSV in-browserFully free (MIT)Free, open-source
@dnd-kit/core + @dnd-kit/sortableDrag-and-drop field reordering on the builder canvasFully free (MIT)Free, open-source
react-hook-form + zodClient-side form state management and validation in both the builder and rendererFully free (MIT)Free, open-source
n8n Cloud (optional)Webhook automation triggered on new form submission — send to Slack, create CRM record, email notificationSelf-hosted freeStarter €20/mo (approx)

Swipe the table sideways to see pricing.

What it costs to run

Drag through the tiers to see how your monthly bill scales with users — no surprises later.

Estimated monthly running cost

$0/mo

Supabase free tier handles schema storage and all submissions at this scale. All rendering libraries are free. No paid services needed unless file uploads push Storage past 1GB.

Estimates use verified 2026 pricing and assume typical usage per user. Your real bill depends on activity, storage, and third-party plans.

What breaks when AI tools build this

The failures people actually hit on their first build — the symptom, why it happens, and the exact fix.

JSONB schema not validated on insert causes silent renderer crashes

Symptom: 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

Symptom: 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

Symptom: 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

Symptom: 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

Symptom: 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

1

Validate the entire form schema with zod before every Supabase upsert — never trust the builder canvas to produce valid JSON without a schema guard

2

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

3

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

4

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

5

Store webhook URLs in the form's settings JSONB rather than hardcoding them — this lets form owners configure integrations without a code deploy

6

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

7

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

8

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

When You Need Custom Development

AI tools handle standard form-and-collect workflows well. A few requirements push past what any AI-generated build reliably sustains:

  • Form schema needs versioning — published forms must be editable without breaking existing submission data or the submissions dashboard column layout
  • Team collaboration required: multiple users editing the same form simultaneously with real-time cursor presence and conflict resolution
  • White-label embedding: forms must load on third-party sites via a JavaScript snippet with per-tenant CSS theming and no RapidDev branding
  • Compliance requirements (GDPR data deletion, HIPAA, SOC 2) that need submission data encryption at rest, per-user data export packages, and automated deletion workflows

RapidDev builds these features for production

Real auth, database, and payments — fixed price, delivered in weeks. You own 100% of the code.

Get a fixed-price quote

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.

RapidDev

Need this feature production-ready?

RapidDev builds a dynamic form builder into real apps — auth, database, payments — at $13K–$25K.

Book a free consultation

30-min call. No commitment.

Want this built for you?

We ship production apps at a fixed price — $13K–$25K, 6–10 weeks, source code yours. You've seen what it takes; we do it every week.

Get a fixed-price quote

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.