Blog / AI Tool Development

Authentication with Bolt: From Mock Users to Real Auth

Learn how to implement real user authentication in your Bolt.new app. Complete guide covering Supabase Auth, OAuth providers, route protection, and security best practices.

ShipAi Team
15 min read
Authentication with Bolt: From Mock Users to Real Auth

You built a Bolt app with a login page, a user dashboard, and personalized features. There’s just one problem: the “user” is a hardcoded object sitting in your component state. Click “Log Out” and refresh the page—you’re logged right back in.

Mock authentication is fine for prototyping. It lets you build out the UI and test user flows without the overhead of setting up a real auth system. But eventually, you need actual users with actual accounts. People who can sign up, log in from different devices, reset their passwords, and trust that their data is secure.

This guide walks through implementing real authentication in a Bolt app. We’ll focus on Supabase Auth since it integrates smoothly with React/Next.js and offers a generous free tier, but the concepts apply to any auth provider.

Why Authentication Is Harder Than It Looks

At first glance, auth seems simple: check if an email and password match, then set a cookie. In practice, there are dozens of edge cases and security considerations that make building your own auth system risky.

Building Auth Yourself Requires

  • • Secure password hashing (bcrypt, argon2)
  • • Session management and token rotation
  • • Password reset flow with secure tokens
  • • Email verification system
  • • Rate limiting for login attempts
  • • CSRF protection
  • • XSS prevention in token storage
  • • OAuth implementation for social logins

Using an Auth Provider Gives You

  • • All of the above, battle-tested
  • • Social login in minutes, not weeks
  • • Built-in email templates
  • • Session management handled automatically
  • • Regular security updates
  • • Compliance certifications (SOC 2, GDPR)
  • • MFA support out of the box
  • • Audit logs and admin dashboards

The consensus among security professionals is clear: don’t build your own auth. Use a proven solution and focus on what makes your app unique.

Choosing an Auth Provider

Several options work well with Bolt apps. Here’s how they compare:

ProviderFree TierBest ForNotable Features
Supabase Auth50,000 MAUsFull-stack apps with databaseIntegrated with Supabase DB, RLS
Auth07,000 MAUsEnterprise, complex requirementsRules engine, extensive integrations
Clerk10,000 MAUsReact/Next.js appsPre-built components, easy setup
Firebase AuthNo MAU limitMobile apps, Google ecosystemPhone auth, anonymous auth
NextAuth.jsSelf-hosted (free)Full control, Next.js nativeBring your own database

For most Bolt apps, Supabase Auth offers the best balance of features, pricing, and developer experience. Let’s walk through implementation.

Setting Up Supabase Auth

Step 1: Create a Supabase Project

  1. 1. Go to supabase.com and create an account
  2. 2. Click “New Project” and fill in the details
  3. 3. Wait for the database to provision (about 2 minutes)
  4. 4. Navigate to Project Settings → API
  5. 5. Copy your Project URL and anon/public key

Step 2: Configure Environment Variables

# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key

Step 3: Install the Supabase Client

npm install @supabase/supabase-js @supabase/ssr

Step 4: Create the Supabase Client

For Next.js App Router, you need both a browser client and a server client:

// lib/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr'

export function createClient() {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  )
}
// lib/supabase/server.ts
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { cookies } from 'next/headers'

export async function createClient() {
  const cookieStore = await cookies()

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll()
        },
        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options)
            )
          } catch {
            // Called from Server Component, ignore
          }
        },
      },
    }
  )
}

Building the Authentication Flow

Sign Up Form

// app/signup/page.tsx
'use client'

import { useState } from 'react'
import { createClient } from '@/lib/supabase/client'
import { useRouter } from 'next/navigation'

export default function SignUpPage() {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [error, setError] = useState<string | null>(null)
  const [loading, setLoading] = useState(false)
  const router = useRouter()
  const supabase = createClient()

  const handleSignUp = async (e: React.FormEvent) => {
    e.preventDefault()
    setLoading(true)
    setError(null)

    const { error } = await supabase.auth.signUp({
      email,
      password,
      options: {
        emailRedirectTo: `${window.location.origin}/auth/callback`,
      },
    })

    if (error) {
      setError(error.message)
      setLoading(false)
      return
    }

    // Redirect to check email page
    router.push('/signup/check-email')
  }

  return (
    <div className="max-w-md mx-auto mt-20 p-8">
      <h1 className="text-2xl font-bold mb-6">Create an Account</h1>

      <form onSubmit={handleSignUp} className="space-y-4">
        <div>
          <label className="block text-sm font-medium mb-1">Email</label>
          <input
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            className="w-full px-4 py-2 border rounded-lg"
            required
          />
        </div>

        <div>
          <label className="block text-sm font-medium mb-1">Password</label>
          <input
            type="password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
            className="w-full px-4 py-2 border rounded-lg"
            minLength={8}
            required
          />
        </div>

        {error && (
          <div className="text-red-600 text-sm">{error}</div>
        )}

        <button
          type="submit"
          disabled={loading}
          className="w-full bg-indigo-600 text-white py-2 rounded-lg
                     hover:bg-indigo-700 disabled:opacity-50"
        >
          {loading ? 'Creating account...' : 'Sign Up'}
        </button>
      </form>
    </div>
  )
}

Login Form

// app/login/page.tsx
'use client'

import { useState } from 'react'
import { createClient } from '@/lib/supabase/client'
import { useRouter } from 'next/navigation'

export default function LoginPage() {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [error, setError] = useState<string | null>(null)
  const [loading, setLoading] = useState(false)
  const router = useRouter()
  const supabase = createClient()

  const handleLogin = async (e: React.FormEvent) => {
    e.preventDefault()
    setLoading(true)
    setError(null)

    const { error } = await supabase.auth.signInWithPassword({
      email,
      password,
    })

    if (error) {
      setError(error.message)
      setLoading(false)
      return
    }

    router.push('/dashboard')
    router.refresh()
  }

  return (
    <div className="max-w-md mx-auto mt-20 p-8">
      <h1 className="text-2xl font-bold mb-6">Welcome Back</h1>

      <form onSubmit={handleLogin} className="space-y-4">
        <div>
          <label className="block text-sm font-medium mb-1">Email</label>
          <input
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            className="w-full px-4 py-2 border rounded-lg"
            required
          />
        </div>

        <div>
          <label className="block text-sm font-medium mb-1">Password</label>
          <input
            type="password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
            className="w-full px-4 py-2 border rounded-lg"
            required
          />
        </div>

        {error && (
          <div className="text-red-600 text-sm">{error}</div>
        )}

        <button
          type="submit"
          disabled={loading}
          className="w-full bg-indigo-600 text-white py-2 rounded-lg
                     hover:bg-indigo-700 disabled:opacity-50"
        >
          {loading ? 'Signing in...' : 'Log In'}
        </button>
      </form>

      <p className="mt-4 text-sm text-gray-600">
        <a href="/forgot-password" className="text-indigo-600 hover:underline">
          Forgot your password?
        </a>
      </p>
    </div>
  )
}

Auth Callback Handler

After email confirmation, Supabase redirects users back to your app. You need a route to handle this:

// app/auth/callback/route.ts
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'

export async function GET(request: Request) {
  const { searchParams, origin } = new URL(request.url)
  const code = searchParams.get('code')
  const next = searchParams.get('next') ?? '/dashboard'

  if (code) {
    const supabase = await createClient()
    const { error } = await supabase.auth.exchangeCodeForSession(code)

    if (!error) {
      return NextResponse.redirect(`${origin}${next}`)
    }
  }

  // Return to login with error
  return NextResponse.redirect(`${origin}/login?error=auth_failed`)
}

Adding Social Login (OAuth)

Social login dramatically reduces friction. Users can sign up with one click instead of creating yet another password. Here’s how to add Google OAuth:

Step 1: Configure in Supabase

  1. 1. In Supabase Dashboard, go to Authentication → Providers
  2. 2. Enable Google and configure with your Google Cloud OAuth credentials
  3. 3. Add your redirect URL to Google Cloud Console: https://your-project.supabase.co/auth/v1/callback

Step 2: Add OAuth Button

// components/GoogleLoginButton.tsx
'use client'

import { createClient } from '@/lib/supabase/client'

export default function GoogleLoginButton() {
  const supabase = createClient()

  const handleGoogleLogin = async () => {
    await supabase.auth.signInWithOAuth({
      provider: 'google',
      options: {
        redirectTo: `${window.location.origin}/auth/callback`,
      },
    })
  }

  return (
    <button
      onClick={handleGoogleLogin}
      className="w-full flex items-center justify-center gap-2 px-4 py-2
                 border border-gray-300 rounded-lg hover:bg-gray-50"
    >
      <svg className="w-5 h-5" viewBox="0 0 24 24">
        {/* Google icon SVG */}
      </svg>
      Continue with Google
    </button>
  )
}

Protecting Routes

Once users can log in, you need to protect routes that require authentication. In Next.js App Router, the cleanest approach uses middleware.

Middleware for Route Protection

// middleware.ts
import { createServerClient, type CookieOptions } 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()

  // Protected routes
  const protectedPaths = ['/dashboard', '/settings', '/profile']
  const isProtectedPath = protectedPaths.some(path =>
    request.nextUrl.pathname.startsWith(path)
  )

  if (isProtectedPath && !user) {
    const redirectUrl = new URL('/login', request.url)
    redirectUrl.searchParams.set('next', request.nextUrl.pathname)
    return NextResponse.redirect(redirectUrl)
  }

  // Redirect logged-in users away from auth pages
  const authPaths = ['/login', '/signup']
  const isAuthPath = authPaths.includes(request.nextUrl.pathname)

  if (isAuthPath && user) {
    return NextResponse.redirect(new URL('/dashboard', request.url))
  }

  return supabaseResponse
}

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

Server Component Auth Check

For server components, you can check auth status directly:

// app/dashboard/page.tsx
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'

export default async function DashboardPage() {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()

  if (!user) {
    redirect('/login')
  }

  return (
    <div className="p-8">
      <h1 className="text-2xl font-bold">Welcome, {user.email}</h1>
      {/* Dashboard content */}
    </div>
  )
}

Implementing Password Reset

Users will forget their passwords. You need a secure reset flow.

// app/forgot-password/page.tsx
'use client'

import { useState } from 'react'
import { createClient } from '@/lib/supabase/client'

export default function ForgotPasswordPage() {
  const [email, setEmail] = useState('')
  const [sent, setSent] = useState(false)
  const [error, setError] = useState<string | null>(null)
  const supabase = createClient()

  const handleReset = async (e: React.FormEvent) => {
    e.preventDefault()
    setError(null)

    const { error } = await supabase.auth.resetPasswordForEmail(email, {
      redirectTo: `${window.location.origin}/reset-password`,
    })

    if (error) {
      setError(error.message)
      return
    }

    setSent(true)
  }

  if (sent) {
    return (
      <div className="max-w-md mx-auto mt-20 p-8 text-center">
        <h1 className="text-2xl font-bold mb-4">Check Your Email</h1>
        <p className="text-gray-600">
          We sent a password reset link to {email}
        </p>
      </div>
    )
  }

  return (
    <div className="max-w-md mx-auto mt-20 p-8">
      <h1 className="text-2xl font-bold mb-6">Reset Your Password</h1>

      <form onSubmit={handleReset} className="space-y-4">
        <div>
          <label className="block text-sm font-medium mb-1">Email</label>
          <input
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            className="w-full px-4 py-2 border rounded-lg"
            required
          />
        </div>

        {error && <div className="text-red-600 text-sm">{error}</div>}

        <button
          type="submit"
          className="w-full bg-indigo-600 text-white py-2 rounded-lg"
        >
          Send Reset Link
        </button>
      </form>
    </div>
  )
}

Role-Based Access Control

For apps with different user types (admin, user, moderator), you’ll need role-based access control. Supabase supports this through custom claims in the JWT.

Setting Up Roles

-- Create a profiles table with role
CREATE TABLE profiles (
  id UUID REFERENCES auth.users PRIMARY KEY,
  email TEXT,
  role TEXT DEFAULT 'user' CHECK (role IN ('user', 'admin', 'moderator')),
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Trigger to create profile on signup
CREATE OR REPLACE FUNCTION handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
  INSERT INTO profiles (id, email)
  VALUES (NEW.id, NEW.email);
  RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER on_auth_user_created
  AFTER INSERT ON auth.users
  FOR EACH ROW EXECUTE FUNCTION handle_new_user();

Checking Roles in Your App

// lib/auth.ts
import { createClient } from '@/lib/supabase/server'

export async function getUserRole() {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()

  if (!user) return null

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

  return profile?.role ?? 'user'
}

export async function requireAdmin() {
  const role = await getUserRole()
  if (role !== 'admin') {
    throw new Error('Unauthorized')
  }
}

Security Best Practices

1. Always Use HTTPS

Cookies and tokens can be intercepted over HTTP. Production apps must use HTTPS exclusively.

2. Validate Server-Side

Never trust client-side auth state alone. Always verify the session on the server before returning sensitive data.

3. Implement Session Timeout

Configure reasonable session durations. For sensitive apps, consider requiring re-authentication for critical actions.

4. Enable MFA for Admin Accounts

Admin accounts are high-value targets. Require multi-factor authentication for elevated privileges.

5. Log Authentication Events

Keep audit logs of login attempts, password changes, and suspicious activity for security monitoring.

Common Mistakes to Avoid

Mistake: Storing Tokens in localStorage

localStorage is accessible to any JavaScript on the page, making it vulnerable to XSS attacks.

Solution: Let your auth provider handle token storage. Supabase uses httpOnly cookies by default.

Mistake: Not Handling Token Expiration

JWTs expire. If you don’t handle this gracefully, users get mysterious errors.

Solution: Supabase automatically refreshes tokens. Make sure you’re using the latest SDK.

Mistake: Weak Password Requirements

Allowing “password123” puts your users at risk.

Solution: Require minimum 8 characters. Consider strength indicators without being overly restrictive.

Mistake: Exposing User Enumeration

Messages like “No account with this email” tell attackers which emails are registered.

Solution: Use generic messages: “If an account exists, we’ll send a reset link.”

Wrapping Up

Authentication is foundational infrastructure. Get it right, and your users can trust your app with their data. Get it wrong, and you risk breaches, compliance issues, and lost trust.

Key Takeaways:

  • • Use a proven auth provider instead of building your own
  • • Supabase Auth works well for most Bolt apps
  • • Social login reduces signup friction significantly
  • • Protect routes with middleware for consistent security
  • • Always validate auth state server-side
  • • Handle password reset and MFA for a complete solution

With proper authentication in place, your Bolt prototype becomes a real application that can safely handle user data. This is one of the essential steps in moving from demo to production.

Need Help With Authentication?

Setting up auth correctly is critical. If you want expert help implementing authentication for your Bolt app, we’re here to assist.

Schedule a Call

Ready to Build Your MVP?

Need help turning your idea into reality? Our team has built 50+ successful startup MVPs and knows exactly what it takes to validate your idea quickly and cost-effectively.