An open-source TypeForm clone built with Next.js 16, Supabase, and Tailwind CSS.
- Next.js 16 (App Router) with React 19
- Supabase for auth (Google OAuth + Magic Link) and PostgreSQL database
- Tailwind CSS v4 + shadcn/ui for styling
- Framer Motion for animations
- Cloudflare R2 for file uploads (optional)
app/
├── (auth)/ # Login page (public)
├── (dashboard)/ # Protected: dashboard, form builder, responses, settings
├── api/upload/ # R2 file upload endpoint
├── auth/callback/ # Supabase OAuth callback
├── f/[slug]/ # Public form player (excluded from auth middleware)
components/
├── dashboard/ # Nav, form cards, delete button
├── form-builder/ # FormBuilder, QuestionEditor, FormPreview
├── form-player/ # FormPlayer, QuestionRenderer
├── responses/ # ResponsesDashboard (table view, CSV export)
├── ui/ # shadcn/ui primitives
lib/
├── supabase/ # client.ts (browser), server.ts (RSC), middleware.ts
├── database.types.ts # TypeScript types matching Supabase schema
├── questions.ts # Question type definitions and factory
├── themes.ts # 6 theme presets with CSS variable generation
// Server Components / Route Handlers
import { createClient } from '@/lib/supabase/server'
const supabase = await createClient()
// Client Components
'use client'
import { createClient } from '@/lib/supabase/client'
const supabase = createClient()Three tables with Row Level Security (RLS):
- profiles - User data (auto-created via trigger on auth.users insert)
- forms - Form config with JSONB
questionsarray, theme preset, status (draft/published/closed) - responses - JSONB
answerskeyed by question ID
The form player (form-player.tsx) implements TypeForm's signature experience:
- One question at a time - Full-screen focus with animated transitions
- Keyboard navigation - Enter to advance, Arrow keys/scroll wheel to navigate
- Progress indicator - Top bar showing completion percentage
- Themed experience - Dynamic CSS variables from theme config
- Validation - Per-question with type-specific rules (email, URL, phone patterns)
13 question types defined in lib/questions.ts. Each has:
- Type identifier (snake_case:
short_text,opinion_scale, etc.) - Label, description, icon (Lucide)
- Default config (placeholder, options, min/max values)
Add new types by:
- Add to
QuestionTypeunion indatabase.types.ts - Add entry to
questionTypesarray inquestions.ts - Add renderer case in
question-renderer.tsx
6 presets in lib/themes.ts: midnight, ocean, sunset, forest, lavender, minimal
Each theme defines: primaryColor, backgroundColor, textColor, accentColor, fontFamily
Use getThemeCSSVariables() to apply as inline CSS custom properties.
- 'use client' only where needed (interactivity, hooks, browser APIs)
- Server Components for data fetching and auth checks
- Route groups
(auth)and(dashboard)for layout organization - Form state uses React
useState- no external form library for simple cases - Toast notifications via
sonner(toast.success(),toast.error()) - Animations use Framer Motion's
motioncomponents withAnimatePresence - Reorderable lists use
framer-motion'sReordercomponent
- User visits
/login→ Google OAuth or Magic Link - Supabase redirects to
/auth/callback→ exchanges code for session - Middleware (
lib/supabase/middleware.ts) refreshes session on protected routes - Dashboard layout checks auth, redirects to
/loginif unauthenticated
Forms at /f/[slug] are excluded from auth middleware. They:
- Fetch published forms via Supabase with RLS policy allowing public read
- Submit responses without authentication
- Display "not found" for unpublished/missing forms
Configure R2 env vars to enable. Upload endpoint at app/api/upload/route.ts returns public URL stored in answers JSONB.
npm run dev # Development server (localhost:3000)
npm run build # Production build
npm run lint # ESLint