Build an Auth & Onboarding System with Claude Code
Build an Auth & Onboarding System with Claude Code
What you'll build
A full signup flow with Google OAuth, a 4-step onboarding wizard, and automatic CRM tagging — so every user who signs up gets tracked, profiled, and emailed.
The Problem
You launched your app. People can see it. Nobody can sign up. You need login pages, Google OAuth, a database for user profiles, an onboarding flow that captures who your users are, and you want every signup automatically tagged in your email marketing tool. Setting all that up from scratch sounds like a week of work. Let's do it in 25 minutes.
What You're Building
A Next.js app with Supabase auth (Google + email), a profiles database that auto-creates on signup, a 4-step onboarding wizard that saves preferences, and an event system that syncs every user to ActiveCampaign with tags like "beginner_level" and "goal_build-tools." When you're done, a user clicks "Sign in with Google," lands in onboarding, answers a few questions, and ends up fully tracked in your CRM — without you writing a single SQL query by hand.
Milestone 1: Supabase Auth Foundation
Every user needs a profile. Instead of manually creating one after signup, we'll let the database handle it automatically.
Prompt:
Set up Supabase auth for this Next.js project. Create a profiles table that extends auth.users — fields: id (references auth.users), email, full_name, avatar_url, experience_level, goal, onboarding_completed (boolean, default false), created_at, updated_at. Add a database trigger that auto-creates a profile row whenever a new user signs up — pull name and avatar from the auth metadata. Add an updated_at trigger too. Add RLS policies: anyone can read profiles, users can only update their own. Create three Supabase client helpers in lib/supabase/: a browser client (client.ts) using createBrowserClient, a server client using cookies (server.ts) using createServerClient, and an admin client using the service role key (admin.ts) using createClient from @supabase/supabase-js. Use @supabase/ssr for the browser and server clients.
What Claude Code does: It creates the SQL migration with a handle_new_user() trigger function — the key concept here. When Supabase Auth creates a user, PostgreSQL fires the trigger and inserts a profile row automatically. No extra API call needed. It also sets up three client helpers: browser-side for React components, server-side for API routes, and admin for operations that bypass row-level security.
Try it: Go to your Supabase dashboard → SQL Editor. Paste the generated migration SQL and run it. Then check Table Editor — you should see the profiles table. Add your Supabase keys to .env.local:
NEXT_PUBLIC_SUPABASE_URL=your-url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
Milestone 2: Login & Register Pages
Users need a way in. Google OAuth at the top, email as a fallback.
Prompt:
Build login and register pages. Both should have a Google OAuth button at the top, a divider saying "or continue with email", then email/password fields. Register also needs a full name field. For OAuth, use supabase.auth.signInWithOAuth with provider "google" and redirect to /api/auth/callback. For email login, use signInWithPassword. For registration, create a server API route at /api/auth/register that uses the admin client to create users with email_confirm: true (no verification email needed), then the client signs them in with signInWithPassword immediately. Add middleware that protects /dashboard and /onboarding — redirect unauthenticated users to /login, redirect authenticated users away from login/register pages.
What Claude Code does: It builds two pages and a registration API. The critical pattern is server-side user creation — instead of using Supabase's client-side signup (which sends a verification email you don't want), we use the admin client with email_confirm: true. The user is created and confirmed in one step. The middleware uses supabase.auth.getUser() on every request to protected routes.
Try it: Run npm run dev and visit /register. You should see the Google button and email form. Register with an email — you should land on the dashboard with no verification email sent.
Milestone 3: OAuth Callback & Profile Pre-fill
When Google sends the user back, we exchange the code for a session and grab their name and photo.
Prompt:
Create an API route at /api/auth/callback. It receives a "code" query parameter from the OAuth redirect. Exchange it for a session using supabase.auth.exchangeCodeForSession(code). Then check if the user's profile has onboarding_completed set to false. If so, grab full_name and avatar_url from user.user_metadata (Google provides "full_name" and "picture") and update the profiles table. Redirect new users to /onboarding. If onboarding is already complete, redirect to /dashboard.
What Claude Code does: The callback handles the OAuth code exchange — Google redirects back with a temporary code, and Supabase trades it for a persistent session. Then it reads the user's Google metadata (name, profile photo) and updates the profile. When the user arrives at onboarding, their name and avatar are already there.
Try it: You'll need Google OAuth credentials first. Go to Google Cloud Console → APIs & Credentials → Create OAuth Client ID. Set the authorized redirect URI to https://your-project.supabase.co/auth/v1/callback. Then in Supabase Dashboard → Auth → Providers → Google, paste the client ID and secret. Now click "Sign in with Google" on your login page — you should land on /onboarding with your name visible.
Milestone 4: Onboarding Wizard
Users can sign up. Now let's learn who they are with a fast, skippable wizard.
Prompt:
Build a 4-step onboarding page. Step 1: "What's your coding experience?" with three clickable cards — Beginner, Intermediate, Advanced — clicking one auto-advances to step 2. Step 2: "What's your main goal?" with four cards — Build tools, Automate work, Learn Claude Code, Portfolio projects — clicking auto-advances to step 3. Step 3: "Tell us about yourself" with optional fields: job title, company, country, a "how did you hear about us" dropdown (Google, LinkedIn, Twitter/X, YouTube, Friend, Newsletter, Other — show a text input when Other is selected), and a newsletter opt-in checkbox (default checked). Include Continue and Skip buttons. Step 4: "You're all set!" showing a recommended tutorial with a Start button. Add a Skip button on steps 1 and 2 as well. Progress dots at the top. Pre-fill user name and avatar from supabase.auth.getUser() metadata. On step 3 submit, POST to /api/onboarding with all collected data.
What Claude Code does: The whole wizard is one component managing step state — no page navigations. Each step is a conditional render based on a step variable. Clicking a card calls setStep(next) instantly. No loading, no routing, no API calls until the final submit. This keeps the experience snappy — each click feels immediate.
Try it: Sign in and you should land on the onboarding page. Your Google name and avatar should show on step 1. Click through the steps. Try skipping every step — you should reach the recommendation screen. Try filling everything in — same result, different data saved.
Milestone 5: Onboarding API & CRM Sync
The answers need to go two places: your database and your CRM.
Prompt:
Create two things. First, a lib/activecampaign.ts module with these functions: syncContact(email, firstName, lastName) that POSTs to /api/3/contact/sync to create or update a contact; addToList(contactId, listId) that adds a contact to a list; addTag(contactId, tagName) that finds or creates a tag then assigns it to the contact; addTags(contactId, tags[]) that runs addTag in parallel for multiple tags. Use the ACTIVECAMPAIGN_URL and ACTIVECAMPAIGN_API_KEY env vars with an "Api-Token" header. Second, create an API route at /api/onboarding. Validate with Zod — all fields optional: experience_level, goal, job_title, company, country, heard_from, newsletter_opt_in. Save to profiles table (set onboarding_completed: true). Sync to ActiveCampaign: create/update the contact, add a tag for every non-empty response like "beginner_level", "goal_build-tools", "company_acme", "heard_from_google". If newsletter_opt_in is true, add them to the newsletter list. Return a recommended tutorial slug based on their goal.
What Claude Code does: The AC client uses the contact sync endpoint — an upsert that creates new contacts or updates existing ones by email. No "check if exists" step needed. Every onboarding response becomes a CRM tag, so you can segment users in email campaigns: email all beginners, email everyone who heard about you from LinkedIn, email users who want to automate work.
Try it: Add your ActiveCampaign keys to .env.local (ACTIVECAMPAIGN_URL and ACTIVECAMPAIGN_API_KEY). Complete onboarding, then check ActiveCampaign → Contacts. Search for your email — you should see tags like "onboarded", "beginner_level", "goal_build-tools".
Milestone 6: Event System & Welcome Email
Every signup should trigger a welcome email and CRM tags — without slowing down the signup itself.
Prompt:
Build a fire-and-forget event system. Create lib/behavioral-tags.ts with event types: user.signup, user.onboarded, tutorial.started, tutorial.completed, subscription.created. Each event maps to ActiveCampaign tags — user.signup maps to "signed_up" plus a country tag if available. Create lib/events.ts with a fireUserEvent function that POSTs to /api/webhooks/user-events — use fetch() without await so it doesn't block. Build the webhook route at /api/webhooks/user-events: receive the event, sync the contact to ActiveCampaign, apply the mapped tags, add signup users to the newsletter list (list ID in a constant), and send a welcome email for user.signup events. Wire up fireUserEvent in the OAuth callback and the registration route — call it after creating/authenticating the user.
What Claude Code does: The fire-and-forget pattern is everything here. fireUserEvent calls fetch() without await — the signup response returns to the user immediately while the webhook processes tags, list enrollment, and emails in the background. Your user sees a fast signup. Behind the scenes, they're tagged, listed, and emailed.
Try it: Delete your test user from Supabase (Auth → Users) and sign up again. Check your inbox — welcome email. Check ActiveCampaign — contact with "signed_up" tag, on your newsletter list. The signup itself should feel instant.
What You Built
Remember "nobody can sign up"? You just solved it. Your app now has:
- Google OAuth and email registration — users pick their path
- Auto-created profiles with name and avatar pulled from Google
- A 4-step onboarding wizard that saves preferences to your database
- Every response tagged in ActiveCampaign for email segmentation
- Welcome emails firing on signup without slowing down the experience
- Protected routes via middleware
The patterns you used: database triggers for auto-profile creation, OAuth metadata for pre-filling profiles, client-side multi-step forms, Zod validation, CRM tagging for segmentation, and fire-and-forget events for async processing.
Take It Further
- Add GitHub OAuth — one more provider in Supabase, the same callback route handles it automatically
- Detect country from IP — Vercel gives you
x-vercel-ip-countryfor free in request headers. Save it to the profile and tag it in ActiveCampaign on signup. - Add referral tracking — store a referral code from URL params in a cookie, credit the referrer when the referred user signs up
Ready to build your first AI agent?
Live Zoom workshop + 1 month WhatsApp follow-up with Yuval Keshtcher (Hebrew)
Learn about the Workshop