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.

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:
| Provider | Free Tier | Best For | Notable Features |
|---|---|---|---|
| Supabase Auth | 50,000 MAUs | Full-stack apps with database | Integrated with Supabase DB, RLS |
| Auth0 | 7,000 MAUs | Enterprise, complex requirements | Rules engine, extensive integrations |
| Clerk | 10,000 MAUs | React/Next.js apps | Pre-built components, easy setup |
| Firebase Auth | No MAU limit | Mobile apps, Google ecosystem | Phone auth, anonymous auth |
| NextAuth.js | Self-hosted (free) | Full control, Next.js native | Bring 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. Go to supabase.com and create an account
- 2. Click “New Project” and fill in the details
- 3. Wait for the database to provision (about 2 minutes)
- 4. Navigate to Project Settings → API
- 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. In Supabase Dashboard, go to Authentication → Providers
- 2. Enable Google and configure with your Google Cloud OAuth credentials
- 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 CallReady 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.