Fill.in is a collaborative form builder built with Next.js App Router + Supabase. It lets users create forms, publish them to a public URL, collect responses, and collaborate with other editors.
- Production URL:
https://fill-in-ten.vercel.app - Public runtime pattern:
/f/[slug]
If you are evaluating this project for hiring/interview purposes, use the verification checklist below.
- Drag-and-drop form builder
- 12 block types:
- short text, long text, multiple choice
- email, phone, date, link
- number, rating, file upload
- time, linear scale
- Conditional behavior:
- visibility rules (show/hide blocks)
- logic jumps (branching)
- Public runtime rendering from stored JSON schema
- Responses table + analytics dashboard
- Collaboration model (owner + editors)
- Autosave editor workflow
- Next.js 16 (App Router), React 19, TypeScript
- Tailwind CSS v4, Radix UI, Framer Motion,
- Supabase (Postgres, Auth, Storage)
- dnd-kit
- Server routes enforce auth/access and fetch form metadata.
- Builder stores schema in
forms.schemaand autosaves edits. - Runtime
/f/[slug]renders schema and writes responses/events. - Responses page aggregates submissions and event analytics.
See:
/landing/login,/signup,/auth/callbackauth/dashboardform listing/createcreate new form/create/[slug]edit form/create/[slug]/responsesresponses + analytics/f/[slug]public runtime/api/invitecollaborator invite API
- Node.js 20+
- npm 10+
- Supabase project
npm installCreate .env.local:
NEXT_PUBLIC_SUPABASE_URL=...
NEXT_PUBLIC_SUPABASE_ANON_KEY=...
SUPABASE_SERVICE_ROLE_KEY=...
NEXT_PUBLIC_SITE_URL=http://localhost:3000Notes:
SUPABASE_SERVICE_ROLE_KEYis server-only. Never expose it in client code.- Keep
.env.localout of version control.
npm run devApp runs at http://localhost:3000.
Run this in Supabase SQL Editor:
create extension if not exists pgcrypto;
create table if not exists public.forms (
id uuid primary key default gen_random_uuid(),
slug text unique not null,
title text not null default '',
description text not null default '',
status text not null default 'draft' check (status in ('draft', 'published')),
schema jsonb not null,
user_id uuid,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create table if not exists public.form_members (
id uuid primary key default gen_random_uuid(),
form_id uuid not null references public.forms(id) on delete cascade,
user_id uuid not null,
role text not null default 'editor' check (role in ('editor')),
created_at timestamptz not null default now(),
unique (form_id, user_id)
);
create table if not exists public.responses (
id uuid primary key default gen_random_uuid(),
form_id uuid not null references public.forms(id) on delete cascade,
answers jsonb not null,
created_at timestamptz not null default now()
);
create table if not exists public.form_events (
id uuid primary key default gen_random_uuid(),
form_id uuid not null references public.forms(id) on delete cascade,
session_id text,
event_type text not null check (event_type in ('view', 'answer', 'submit')),
block_id text,
created_at timestamptz not null default now()
);Create storage bucket:
- Bucket name:
uploads
Starter runtime upload policy (published forms only):
create policy "runtime_upload_published_forms"
on storage.objects
for insert
to anon, authenticated
with check (
bucket_id = 'uploads'
and exists (
select 1
from public.forms f
where f.id = split_part(name, '/', 1)::uuid
and f.status = 'published'
)
);If bucket is private, add a corresponding select policy for runtime reads.
Use this flow to validate the project quickly:
- Sign in and create a form from
/create. - Add mixed blocks (including file upload and rating).
- Add at least one visibility rule and one logic jump.
- Publish and open
/f/[slug]. - Submit:
- one full response
- one partial response (abandon midway)
- Open
/create/[slug]/responsesand verify:- table data appears
- file/rating previews render
- completion + drop-off metrics update
- Test collaborator invite from form editor.
- Root metadata is in
app/layout.tsx. - Runtime form metadata is dynamic in
app/(runtime)/f/[slug]/page.tsx. robots.txtandsitemap.xmlare generated via:app/robots.tsapp/sitemap.ts
- Auth/builder routes are
noindexvia:app/(auth)/layout.tsxapp/(builder)/layout.tsx
When preview images seem stale on WhatsApp/X, use a new image URL or re-scrape with platform validators (their cache is aggressive).
npm run dev
npm run build
npm run start
npm run lint