Tutorial: Authentication Setup

Add complete user authentication to your Next.js app using Bootspring and Clerk.

What You'll Build#

  • Sign-in and sign-up pages
  • Protected routes
  • User session management
  • Profile dropdown

Prerequisites#

  • Next.js 14 project with App Router
  • Bootspring initialized
  • Clerk account (free tier works)

Time Required#

Approximately 20 minutes.

Step 1: Set Up Clerk#

Create a Clerk Application#

  1. Go to clerk.com and sign up
  2. Create a new application
  3. Select authentication methods (Email, Google, GitHub)
  4. Copy your API keys

Add Environment Variables#

1# .env.local 2NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_... 3CLERK_SECRET_KEY=sk_test_... 4NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in 5NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up 6NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard 7NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/onboarding

Step 2: Apply the Auth Skill#

Use Bootspring's auth skill:

bootspring skill apply auth/clerk

This creates:

  • Middleware configuration
  • Sign-in page
  • Sign-up page
  • Auth layout

Step 3: Install Dependencies#

npm install @clerk/nextjs

Step 4: Configure the Provider#

Ask the auth-expert:

Set up ClerkProvider in the root layout with proper configuration.

Update your layout:

1// app/layout.tsx 2import { ClerkProvider } from '@clerk/nextjs'; 3import { Inter } from 'next/font/google'; 4import './globals.css'; 5 6const inter = Inter({ subsets: ['latin'] }); 7 8export default function RootLayout({ 9 children, 10}: { 11 children: React.ReactNode; 12}) { 13 return ( 14 <ClerkProvider> 15 <html lang="en"> 16 <body className={inter.className}>{children}</body> 17 </html> 18 </ClerkProvider> 19 ); 20}

Step 5: Create Middleware#

Create the middleware:

1// middleware.ts 2import { authMiddleware } from '@clerk/nextjs'; 3 4export default authMiddleware({ 5 // Public routes that don't require authentication 6 publicRoutes: [ 7 '/', 8 '/sign-in(.*)', 9 '/sign-up(.*)', 10 '/api/webhooks(.*)', 11 '/pricing', 12 '/about', 13 ], 14 15 // Routes that can be accessed while signed out but 16 // will have auth data if signed in 17 ignoredRoutes: ['/api/public(.*)'], 18}); 19 20export const config = { 21 matcher: ['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)'], 22};

Step 6: Create Auth Pages#

Sign-In Page#

1// app/(auth)/sign-in/[[...sign-in]]/page.tsx 2import { SignIn } from '@clerk/nextjs'; 3 4export default function SignInPage() { 5 return ( 6 <div className="flex min-h-screen items-center justify-center bg-gray-50"> 7 <SignIn 8 appearance={{ 9 elements: { 10 formButtonPrimary: 11 'bg-blue-600 hover:bg-blue-700 text-sm normal-case', 12 }, 13 }} 14 /> 15 </div> 16 ); 17}

Sign-Up Page#

1// app/(auth)/sign-up/[[...sign-up]]/page.tsx 2import { SignUp } from '@clerk/nextjs'; 3 4export default function SignUpPage() { 5 return ( 6 <div className="flex min-h-screen items-center justify-center bg-gray-50"> 7 <SignUp 8 appearance={{ 9 elements: { 10 formButtonPrimary: 11 'bg-blue-600 hover:bg-blue-700 text-sm normal-case', 12 }, 13 }} 14 /> 15 </div> 16 ); 17}

Auth Layout#

1// app/(auth)/layout.tsx 2export default function AuthLayout({ 3 children, 4}: { 5 children: React.ReactNode; 6}) { 7 return ( 8 <div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100"> 9 {children} 10 </div> 11 ); 12}

Step 7: Add User Button#

Ask the frontend-expert:

Create a header component with user button and navigation.
1// components/Header.tsx 2'use client'; 3 4import { UserButton, SignedIn, SignedOut, SignInButton } from '@clerk/nextjs'; 5import Link from 'next/link'; 6 7export function Header() { 8 return ( 9 <header className="border-b bg-white"> 10 <div className="container mx-auto px-4 py-4 flex items-center justify-between"> 11 <Link href="/" className="text-xl font-bold"> 12 MyApp 13 </Link> 14 15 <nav className="flex items-center gap-6"> 16 <Link href="/features" className="text-gray-600 hover:text-gray-900"> 17 Features 18 </Link> 19 <Link href="/pricing" className="text-gray-600 hover:text-gray-900"> 20 Pricing 21 </Link> 22 23 <SignedIn> 24 <Link 25 href="/dashboard" 26 className="text-gray-600 hover:text-gray-900" 27 > 28 Dashboard 29 </Link> 30 <UserButton afterSignOutUrl="/" /> 31 </SignedIn> 32 33 <SignedOut> 34 <SignInButton mode="modal"> 35 <button className="px-4 py-2 text-blue-600 hover:text-blue-700"> 36 Sign In 37 </button> 38 </SignInButton> 39 <Link 40 href="/sign-up" 41 className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700" 42 > 43 Get Started 44 </Link> 45 </SignedOut> 46 </nav> 47 </div> 48 </header> 49 ); 50}

Step 8: Create Protected Routes#

Dashboard Layout#

1// app/(dashboard)/layout.tsx 2import { auth } from '@clerk/nextjs'; 3import { redirect } from 'next/navigation'; 4import { Sidebar } from '@/components/dashboard/Sidebar'; 5 6export default async function DashboardLayout({ 7 children, 8}: { 9 children: React.ReactNode; 10}) { 11 const { userId } = auth(); 12 13 if (!userId) { 14 redirect('/sign-in'); 15 } 16 17 return ( 18 <div className="flex min-h-screen"> 19 <Sidebar /> 20 <main className="flex-1 p-8">{children}</main> 21 </div> 22 ); 23}

Dashboard Page#

1// app/(dashboard)/dashboard/page.tsx 2import { currentUser } from '@clerk/nextjs'; 3 4export default async function DashboardPage() { 5 const user = await currentUser(); 6 7 return ( 8 <div> 9 <h1 className="text-2xl font-bold mb-4"> 10 Welcome, {user?.firstName || 'User'}! 11 </h1> 12 <p className="text-gray-600"> 13 You're signed in as {user?.emailAddresses[0]?.emailAddress} 14 </p> 15 </div> 16 ); 17}

Step 9: Sync Users to Database#

Create Webhook Endpoint#

Ask the backend-expert:

Create a Clerk webhook endpoint to sync users to our database.
1// app/api/webhooks/clerk/route.ts 2import { Webhook } from 'svix'; 3import { headers } from 'next/headers'; 4import { WebhookEvent } from '@clerk/nextjs/server'; 5import { prisma } from '@/lib/prisma'; 6 7export async function POST(req: Request) { 8 const WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET; 9 10 if (!WEBHOOK_SECRET) { 11 throw new Error('Missing CLERK_WEBHOOK_SECRET'); 12 } 13 14 const headerPayload = headers(); 15 const svix_id = headerPayload.get('svix-id'); 16 const svix_timestamp = headerPayload.get('svix-timestamp'); 17 const svix_signature = headerPayload.get('svix-signature'); 18 19 if (!svix_id || !svix_timestamp || !svix_signature) { 20 return new Response('Missing svix headers', { status: 400 }); 21 } 22 23 const payload = await req.json(); 24 const body = JSON.stringify(payload); 25 26 const wh = new Webhook(WEBHOOK_SECRET); 27 let evt: WebhookEvent; 28 29 try { 30 evt = wh.verify(body, { 31 'svix-id': svix_id, 32 'svix-timestamp': svix_timestamp, 33 'svix-signature': svix_signature, 34 }) as WebhookEvent; 35 } catch (err) { 36 console.error('Webhook verification failed:', err); 37 return new Response('Verification failed', { status: 400 }); 38 } 39 40 const eventType = evt.type; 41 42 if (eventType === 'user.created') { 43 const { id, email_addresses, first_name, last_name, image_url } = evt.data; 44 45 await prisma.user.create({ 46 data: { 47 clerkId: id, 48 email: email_addresses[0]?.email_address, 49 name: [first_name, last_name].filter(Boolean).join(' ') || null, 50 avatarUrl: image_url, 51 }, 52 }); 53 } 54 55 if (eventType === 'user.updated') { 56 const { id, email_addresses, first_name, last_name, image_url } = evt.data; 57 58 await prisma.user.update({ 59 where: { clerkId: id }, 60 data: { 61 email: email_addresses[0]?.email_address, 62 name: [first_name, last_name].filter(Boolean).join(' ') || null, 63 avatarUrl: image_url, 64 }, 65 }); 66 } 67 68 if (eventType === 'user.deleted') { 69 const { id } = evt.data; 70 71 await prisma.user.delete({ 72 where: { clerkId: id }, 73 }); 74 } 75 76 return new Response('Webhook processed', { status: 200 }); 77}

Configure Webhook in Clerk#

  1. Go to Clerk Dashboard → Webhooks
  2. Create a new webhook
  3. URL: https://yourdomain.com/api/webhooks/clerk
  4. Select events: user.created, user.updated, user.deleted
  5. Copy the signing secret to .env.local
CLERK_WEBHOOK_SECRET=whsec_...

Step 10: Test Your Setup#

  1. Start your development server:

    npm run dev
  2. Visit http://localhost:3000

  3. Click "Get Started" to sign up

  4. Complete the sign-up flow

  5. You should be redirected to /dashboard

Verification Checklist#

  • Sign-up works
  • Sign-in works
  • Protected routes redirect to sign-in
  • User button shows in header
  • Sign-out works
  • User synced to database

Security Review#

Run a quick security check:

bootspring agent invoke security-expert "Review the Clerk authentication setup"

The agent will verify:

  • Middleware configuration
  • Public routes are intentional
  • Webhook verification
  • Environment variables secure

What You Learned#

  • Setting up Clerk authentication
  • Creating protected routes
  • Using Clerk components
  • Syncing users to database
  • Webhook handling

Next Steps#

Troubleshooting#

Redirect Loop#

Check that your sign-in pages are in publicRoutes.

User Not Syncing#

  1. Verify webhook URL is correct
  2. Check webhook secret matches
  3. Look at Clerk webhook logs

Middleware Not Working#

Ensure the matcher config includes your routes.

Resources#