# How to Build Authentication system with V0

- Tool: v0
- Difficulty: Intermediate
- Compatibility: V0 Free
- Last updated: April 2026

## TL;DR

Build a multi-provider authentication system with V0 using Next.js and Supabase Auth. You'll get email/password sign-up, Google and GitHub OAuth, protected routes with middleware, profile management, and SSR session handling — all in about 1-2 hours without any local setup.

## Before you start

- A V0 account (free tier works for this project)
- A Supabase project (free tier works — connect via V0's Connect panel)
- Google Cloud Console OAuth credentials (for Google sign-in)
- GitHub OAuth App credentials (for GitHub sign-in)

## Step-by-step guide

### 1. Set up Supabase Auth and the profiles table

Create a new V0 project and connect Supabase via the Connect panel. Then create a custom profiles table that extends the built-in auth.users with app-specific fields. Add a database trigger that automatically creates a profile row when a new user signs up.

```
// Paste this prompt into V0's AI chat:
// Build an authentication system with Supabase Auth. Set up:
// 1. A custom profiles table: id (uuid PK REFERENCES auth.users ON DELETE CASCADE), email (text), full_name (text), avatar_url (text), role (text DEFAULT 'user'), onboarded (boolean DEFAULT false), created_at (timestamptz DEFAULT now())
// 2. A database trigger on auth.users INSERT that auto-creates a profiles row with the user's email
// 3. RLS policies on profiles: users can read any profile, users can update only their own profile
// 4. Enable email/password auth in Supabase (it's on by default)
// Generate the SQL migration for the profiles table, trigger, and RLS policies.
```

> Pro tip: The Connect panel auto-provisions NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY. You will also need SUPABASE_SERVICE_ROLE_KEY in the Vars tab for server-side operations that bypass RLS.

**Expected result:** Supabase is connected, the profiles table is created with a trigger that auto-creates profile rows on signup, and RLS policies are in place.

### 2. Build the login and signup pages with OAuth providers

Create the authentication forms using shadcn/ui components. The login page has email/password fields plus OAuth buttons for Google and GitHub. Both forms use Supabase Auth client methods for the actual authentication.

```
// Paste this prompt into V0's AI chat:
// Build authentication pages:
// 1. app/login/page.tsx - Login form with:
//    - shadcn/ui Card containing the form
//    - Input for email and password with Label components
//    - Button for "Sign in with email"
//    - Separator with "or continue with" text
//    - Two OAuth Buttons: Google (with icon) and GitHub (with icon)
//    - Link to /signup for new users
//    - Use Supabase createBrowserClient for auth methods
//    - signInWithPassword for email, signInWithOAuth for Google/GitHub
//    - Redirect to /dashboard on success
// 2. app/signup/page.tsx - Registration form with:
//    - Same Card layout as login
//    - Additional Input for full_name
//    - signUp method with email, password, and metadata
//    - Show "Check your email" message after signup
//    - Link to /login for existing users
// Both pages should be 'use client' with useState for loading/error states
```

**Expected result:** Login and signup pages render with shadcn/ui Cards, email/password inputs, and OAuth provider buttons. Forms handle loading and error states.

### 3. Create the OAuth callback handler and middleware

Build the callback route that handles OAuth redirects from Google and GitHub. Then create the Next.js middleware that refreshes the Supabase session on every request using cookie-based auth from @supabase/ssr.

```
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'

export async function middleware(request: NextRequest) {
  let supabaseResponse = NextResponse.next({ request })

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return request.cookies.getAll()
        },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value, options }) =>
            request.cookies.set(name, value)
          )
          supabaseResponse = NextResponse.next({ request })
          cookiesToSet.forEach(({ name, value, options }) =>
            supabaseResponse.cookies.set(name, value, options)
          )
        },
      },
    }
  )

  const { data: { user } } = await supabase.auth.getUser()

  if (
    !user &&
    !request.nextUrl.pathname.startsWith('/login') &&
    !request.nextUrl.pathname.startsWith('/signup') &&
    !request.nextUrl.pathname.startsWith('/auth') &&
    request.nextUrl.pathname !== '/'
  ) {
    const url = request.nextUrl.clone()
    url.pathname = '/login'
    return NextResponse.redirect(url)
  }

  return supabaseResponse
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)'],
}
```

> Pro tip: The middleware refreshes the Supabase session cookie on every request. This prevents the session from expiring while the user is active and eliminates the flash of unauthenticated content in Server Components.

**Expected result:** The middleware intercepts all requests, refreshes the session, and redirects unauthenticated users to /login. OAuth callbacks are processed at /auth/callback.

### 4. Build the protected dashboard layout with session data

Create a shared layout for all protected pages that verifies the session server-side and provides user data to child components. This layout also handles the case where a user is authenticated but has not completed onboarding.

```
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import { UserNav } from '@/components/user-nav'

export default async function ProtectedLayout({
  children,
}: {
  children: React.ReactNode
}) {
  const cookieStore = await cookies()
  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll()
        },
      },
    }
  )

  const { data: { user } } = await supabase.auth.getUser()
  if (!user) redirect('/login')

  const { data: profile } = await supabase
    .from('profiles')
    .select('full_name, avatar_url, role, onboarded')
    .eq('id', user.id)
    .single()

  if (!profile?.onboarded) redirect('/onboarding')

  return (
    <div className="min-h-screen">
      <header className="border-b px-6 py-3 flex items-center justify-between">
        <h1 className="text-lg font-semibold">Dashboard</h1>
        <UserNav
          name={profile.full_name}
          email={user.email!}
          avatar={profile.avatar_url}
        />
      </header>
      <main className="p-6">{children}</main>
    </div>
  )
}
```

**Expected result:** All pages under the (protected) route group verify the session, fetch the user profile, redirect to onboarding if needed, and render a header with the user's avatar and name.

### 5. Add the profile management page with avatar upload

Create a profile page where authenticated users can update their name, upload an avatar to Supabase Storage, and view their account details. Use a Server Action for the update to keep the logic server-side.

```
'use server'

import { createClient } from '@supabase/supabase-js'
import { revalidatePath } from 'next/cache'

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
)

export async function updateProfile(
  userId: string,
  fullName: string,
  avatarUrl?: string
) {
  const updates: Record<string, unknown> = { full_name: fullName }
  if (avatarUrl) updates.avatar_url = avatarUrl

  const { error } = await supabase
    .from('profiles')
    .update(updates)
    .eq('id', userId)

  if (error) throw new Error(error.message)
  revalidatePath('/profile')
}

export async function completeOnboarding(userId: string, fullName: string) {
  const { error } = await supabase
    .from('profiles')
    .update({ full_name: fullName, onboarded: true })
    .eq('id', userId)

  if (error) throw new Error(error.message)
  revalidatePath('/')
}
```

**Expected result:** Users can update their name and avatar. The onboarding action marks the profile as complete and redirects to the dashboard.

## Complete code example

File: `middleware.ts`

```typescript
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'

export async function middleware(request: NextRequest) {
  let supabaseResponse = NextResponse.next({ request })

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return request.cookies.getAll()
        },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value }) =>
            request.cookies.set(name, value)
          )
          supabaseResponse = NextResponse.next({ request })
          cookiesToSet.forEach(({ name, value, options }) =>
            supabaseResponse.cookies.set(name, value, options)
          )
        },
      },
    }
  )

  const {
    data: { user },
  } = await supabase.auth.getUser()

  const publicPaths = ['/login', '/signup', '/auth', '/']
  const isPublic = publicPaths.some((p) =>
    request.nextUrl.pathname.startsWith(p)
  )

  if (!user && !isPublic) {
    const url = request.nextUrl.clone()
    url.pathname = '/login'
    return NextResponse.redirect(url)
  }

  return supabaseResponse
}

export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
}
```

## Common mistakes

- **Using createBrowserClient in Server Components** — createBrowserClient relies on browser cookies and does not work in Server Components. The session will always appear as null, causing authenticated users to be redirected to login. Fix: Use createServerClient from @supabase/ssr in Server Components and middleware. Use createBrowserClient only in Client Components marked with 'use client'.
- **Forgetting to set OAuth redirect URLs in the provider console** — Google and GitHub OAuth require exact redirect URLs. If the redirect URL does not match what is configured in the provider console, the OAuth flow fails silently or shows an error. Fix: Set the redirect URL to https://your-supabase-project.supabase.co/auth/v1/callback in both the Google Cloud Console and GitHub OAuth App settings. After deploying to a custom domain, add that URL too.
- **Exposing SUPABASE_SERVICE_ROLE_KEY with NEXT_PUBLIC_ prefix** — The service role key bypasses all Row Level Security. Exposing it in the browser means anyone can read, write, and delete any data in your database. Fix: Store SUPABASE_SERVICE_ROLE_KEY in V0's Vars tab without any prefix. Only SUPABASE_URL and SUPABASE_ANON_KEY should use NEXT_PUBLIC_ if needed on the client.

## Best practices

- Use @supabase/ssr with createServerClient in Server Components and middleware for secure cookie-based session handling
- Use createBrowserClient only in 'use client' components that need to call Supabase Auth methods like signInWithPassword
- Store SUPABASE_SERVICE_ROLE_KEY in V0's Vars tab without NEXT_PUBLIC_ prefix — it should only be accessible server-side
- Add a database trigger on auth.users INSERT to auto-create profiles rows so your app data stays in sync with Supabase Auth
- Use middleware to refresh sessions on every request, preventing session expiry during active use
- Use Design Mode (Option+D) to visually adjust login form layout, OAuth button styling, and card spacing without spending credits
- Always redirect to a meaningful page after OAuth callback — not back to the login page
- Set up RLS policies on the profiles table so users can only update their own profile data

## Frequently asked questions

### Should I use Supabase Auth, Clerk, or Auth.js for my V0 project?

Supabase Auth is the best choice if you are already using Supabase for your database — it keeps everything in one platform with seamless RLS integration. Clerk is fastest if you want pre-built UI components without writing any auth forms. Auth.js is the free open-source option for maximum flexibility.

### How do I prevent the flash of unauthenticated content?

Use the @supabase/ssr middleware pattern that refreshes the session on every request. Server Components check the session before rendering, so unauthenticated users are redirected at the server level before any HTML is sent to the browser.

### What V0 plan do I need for an authentication system?

The V0 free plan works for a basic authentication system. The project requires a few pages (login, signup, profile) and one middleware file, which fits within the free credit allocation.

### How do I add Google OAuth to my V0 project?

First, create OAuth credentials in the Google Cloud Console with the redirect URL pointing to your Supabase project. Then enable Google as a provider in Supabase Dashboard under Authentication, then Providers. The login page calls supabase.auth.signInWithOAuth with provider set to google.

### How do I deploy the auth system to production?

Click Share then Publish to Production in V0. After deployment, update the OAuth redirect URLs in Google and GitHub to include your production domain. Also update the Site URL in Supabase Dashboard under Authentication, then URL Configuration.

### Can I use this authentication system with other databases?

Supabase Auth is tightly integrated with Supabase PostgreSQL. If you want to use a different database, consider Clerk or Auth.js v5 instead, which are database-agnostic and work with any backend.

### Can RapidDev help build a custom authentication system?

Yes. RapidDev has built 600+ apps with complex auth systems including multi-tenant SSO, enterprise SAML, and custom MFA flows. Book a free consultation to discuss your authentication requirements.

---

Source: https://www.rapidevelopers.com/how-to-build-v0/authentication-system
© RapidDev — https://www.rapidevelopers.com/how-to-build-v0/authentication-system
