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#
- Go to clerk.com and sign up
- Create a new application
- Select authentication methods (Email, Google, GitHub)
- 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=/onboardingStep 2: Apply the Auth Skill#
Use Bootspring's auth skill:
bootspring skill apply auth/clerkThis creates:
- Middleware configuration
- Sign-in page
- Sign-up page
- Auth layout
Step 3: Install Dependencies#
npm install @clerk/nextjsStep 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#
- Go to Clerk Dashboard → Webhooks
- Create a new webhook
- URL:
https://yourdomain.com/api/webhooks/clerk - Select events:
user.created,user.updated,user.deleted - Copy the signing secret to
.env.local
CLERK_WEBHOOK_SECRET=whsec_...Step 10: Test Your Setup#
-
Start your development server:
npm run dev -
Visit
http://localhost:3000 -
Click "Get Started" to sign up
-
Complete the sign-up flow
-
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#
- Verify webhook URL is correct
- Check webhook secret matches
- Look at Clerk webhook logs
Middleware Not Working#
Ensure the matcher config includes your routes.