How I Replaced Google Forms with a Custom Contact Form Using Claude Code
Building a Supabase-backed contact form with email notifications in 2 hours — 4x faster than my first database project
Ingredients
- Claude Code — terminal-based AI for direct file editing ($200/yr)
- Supabase — PostgreSQL database for storing contact form submissions (free)
- Resend — email API for instant notifications (free tier: 3,000 emails/month)
- Vercel — hosting with auto-deploy from GitHub (free)
- Next.js — React framework with API routes (free)
- One Google Form iframe — ready to be replaced (free to delete)
The Problem: Google Forms Look Like Google Forms
The contact page had a Google Form embedded in an iframe. It worked, but it looked like every other Google Form on the internet — white background, blue submit button, Google branding at the bottom. I wanted a form that matched the site’s design (cream background, forest green buttons) and sent me instant email notifications without opening the Google Forms dashboard.
The requirements: replace the iframe with a custom form, store submissions in Supabase, send email notifications via Resend, and make it feel native to the site. Having built Numerator two weeks earlier (8–10 hours, also using Supabase), I knew the database setup would be familiar. The question: how much faster with Claude Code in the terminal instead of Claude.ai in the browser?
The Build: One Evening, Four Phases
Pace: Database setup fast (learned from Numerator). Design iteration slow (6 rounds of layout changes). Terminal workflow 4x faster than browser copy-paste.
Phase 1: Database and API (15 minutes)
Every contact form submission needs somewhere to live. The first step was creating a database table — essentially a spreadsheet in the cloud — to capture each submission’s name, email, message, and timestamp. Claude generated a setup script that I ran in Supabase’s web dashboard; no software installation needed on my end.
🔧 Developer section: Database schema and API route
- A
submissionstable with UUID primary key, text fields for name/email/message, a booleanreadflag, and acreated_attimestamp - Indexes on
readstatus andcreated_atfor fast filtering - RLS policies allowing anonymous inserts (anyone can submit) and authenticated reads (only I can view submissions in the dashboard)
- An API route at
/api/contact/route.tswith validation (email format, length limits, required fields)
I ran the SQL in the Supabase SQL Editor, verified the submissions table appeared in Table Editor, and the database was ready. Same flow as Numerator, but this time I knew where to find the SQL Editor and how RLS worked — no re-learning the dashboard.
Claude Code writes files directly to your project via terminal. No downloading code blocks from the browser, no copy-paste into VS Code, no "did I save that file?" checks. It just edits app/api/contact/route.ts and it’s there.
Phase 2: Form Component (30 minutes)
With the database ready, Claude built the visible form — the fields a visitor sees, the submit button, the loading animation while it sends, and the success message when it’s done. Replacing the Google Form meant swapping one line of code (the embedded iframe) with the new component, which Claude handled directly in the terminal.
🔧 Developer section: Form component features
- Name, email, and message fields with React state management
- Client-side validation (required fields, email format, character limits)
- Loading state (button shows "Sending..." while submitting)
- Success message ("Thanks for reaching out! I’ll get back to you soon.")
- Error handling (displays API error messages if submission fails)
- Character counter for the message field (0/1000 characters)
Updated app/contact/page.tsx to replace the Google Form iframe with <ContactForm />. Tested locally at localhost:3000/contact — form loaded, submitted a test, checked Supabase Table Editor and saw the submission. First try worked.
Phase 3: Design Iteration (45 minutes)
The form worked correctly on the first try — the slow part was making it look right. Over six rounds of plain-English feedback, I shaped the layout until it matched the rest of the site. Each round: describe the change, Claude updates the file, refresh the browser.
🔧 Developer section: Design feedback rounds
- Round 1: Split single "Name" field into "First Name" and "Last Name" side by side
- Round 2: Added asterisks (*) to required field labels and a "* REQUIRED FIELD" note at the bottom
- Round 3: Moved "Contact" heading outside the form box, added "Get in touch" intro with 3 bullet points (consulting inquiries, project recommendations, book time to chat)
- Round 4: Removed light green background box, made entire page cream-colored for a cleaner look
- Round 5: Horizontally aligned "* REQUIRED FIELD" text with the character counter (both on the same line)
- Round 6: Centered the "Send Message" button and removed gray borders from input fields for a uniform background feel
Each round: I described the change, Claude Code updated app/globals.css or ContactForm.tsx, I refreshed localhost:3000/contact in the browser, gave feedback. The terminal workflow meant no file switching in VS Code — Claude Code just edited the right file every time.
Design iteration is still the slowest part, even with AI. But Claude Code’s terminal access eliminated all the "which file do I edit?" friction. I gave feedback in plain English, it updated CSS and JSX directly, I refreshed. No context switching.
Phase 4: Email Notifications (30 minutes)
Submissions were saving to the database, but I had no way to know when one arrived without logging in to check. Adding instant email notifications meant connecting an email-sending service — Claude recommended Resend (a free API that sends email programmatically) and wired it into the form’s submit logic.
🔧 Developer section: Resend email integration
- Ran
npm install resendin terminal to add the package - Claude Code updated
/api/contact/route.tsto import Resend and send an email after successful database insert - Created a Resend account, generated an API key, added it to
.env.localalong with my notification email - Tested locally — form submitted, email arrived in my inbox with sender name, email, and message
The email template was simple HTML: sender name, email, message, and timestamp. Good enough for launch.
Deployment: One Command and One Fix
Tested the form locally, verified email notifications worked, then pushed to GitHub. Vercel auto-deployed — and the build failed.
The error: new row violates row-level security policy for table "submissions". The RLS policy I’d written allowed anon role inserts, but Supabase wasn’t recognizing it. I tried recreating the policy, same error. Eventually: disabled RLS entirely on the submissions table (ran ALTER TABLE submissions DISABLE ROW LEVEL SECURITY; in the Supabase SQL Editor).
Submitted the form in production — success message appeared, email arrived, submission logged in Supabase. RLS is worth re-enabling later for security, but disabling it unblocked deployment.
Local dev servers are forgiving. Production builds are not. Supabase RLS policies can fail silently in ways that only surface during deployment. When blocked: disable RLS to ship, re-enable and debug policies later.
One more issue: forgot to add Resend environment variables to Vercel. Went to Vercel dashboard → project Settings → Environment Variables, added RESEND_API_KEY and NOTIFICATION_EMAIL, redeployed. Email notifications worked in production.
One git push → Vercel deployed in 60 seconds → contact form live at joseandgoose.com/contact.
Final Output
A custom contact form at joseandgoose.com/contact with Supabase database storage, Resend email notifications, first/last name fields, character count, validation, success/error messages, and a design that matches the site’s cream and forest green aesthetic — built in 2 hours (vs 8–10 hours for my first Supabase project, Numerator).
What went fast
- Database setup (15 minutes — already knew Supabase from Numerator, just ran the SQL)
- API route creation (Claude Code wrote
/api/contact/route.tswith validation in one shot) - Form component (React state, validation, loading/success states — all worked first try)
- Terminal workflow (no file switching, no copy-paste from browser, Claude Code just edited files directly)
- Email integration (Resend API is dead simple — npm install, add API key, send email)
What needed patience
- Design iteration (6 rounds to get the layout right — split name fields, align text, remove borders, adjust spacing)
- RLS policy debugging (policy syntax looked correct but failed in production — disabled RLS to ship, will re-enable later)
- Environment variables (forgot to add Resend keys to Vercel, had to redeploy)
Claude Code vs Claude.ai: 4x Speed Difference
Numerator took 8–10 hours using Claude.ai in the browser. This contact form took 2 hours using Claude Code in the terminal. Same developer (me), similar complexity (both used Supabase, both had API routes, both required design iteration).
The difference:
- Browser workflow (Numerator): Claude generates code → I copy code block → open VS Code → find the right file → paste → save → refresh browser → give feedback → repeat
- Terminal workflow (Contact Form): I describe change → Claude Code edits the file → I refresh browser → give feedback → repeat
The terminal workflow eliminated 50% of the steps. No context switching between browser and editor. No "which file do I edit?" questions. No copy-paste errors. Just: describe, refresh, iterate.
And because I’d already built Numerator (learned Supabase, understood RLS policies, knew how API routes worked), the second database project was 4x faster. The tools stayed the same. The experience compounded.