This guide walks you through setting up Clerk authentication in your Kit project. By the end, you will have working sign-in, sign-up, social login, and automatic database synchronization.
You can skip this entire page and run
pnpm dev:no-clerk to develop without Clerk. Kit provides a mock authentication system that lets you explore the full dashboard immediately. Come back here when you are ready to connect real authentication.Create a Clerk Account
1
Sign up at Clerk
Go to clerk.com and create a free account. Clerk's free tier is generous — it includes up to 10,000 monthly active users.
2
Create an application
In the Clerk Dashboard, click Create application. Choose a name for your app (e.g., "My SaaS") and select which sign-in methods you want to enable. You can change these later.
At minimum, enable Email address as an identifier. You can also enable Google and GitHub social logins right away — see the Social Login Providers section below.
3
Note your environment
Clerk creates a Development instance by default. This is perfect for local development. When you are ready to go live, you will create a separate Production instance in the Clerk Dashboard.
Get Your API Keys
After creating your application, Clerk shows you two API keys:
- Publishable Key (
pk_test_...) — Safe to expose in the browser. Used by ClerkProvider to initialize the SDK. - Secret Key (
sk_test_...) — Server-only. Used for webhook verification and server-side API calls. Never expose this in client code.
You can find these anytime in the Clerk Dashboard under API Keys.
Never commit
CLERK_SECRET_KEY to version control. It should only exist in apps/boilerplate/.env.local (which is gitignored) and in your hosting platform's environment variables (e.g., Vercel dashboard).Configure Environment Variables
Add your Clerk keys to
apps/boilerplate/.env.local. Kit uses 8 Clerk-related environment variables:.env.example
# IMPORTANT: Replace these empty keys with your actual Clerk keys!
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
CLERK_SECRET_KEY=
CLERK_WEBHOOK_SECRET=
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/login
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/register
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboard
Here is what each variable does:
| Variable | Required | Purpose |
|---|---|---|
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY | Yes | Initializes the Clerk SDK in the browser |
CLERK_SECRET_KEY | Yes | Server-side operations and webhook verification |
CLERK_WEBHOOK_SECRET | Yes | Verifies webhook signatures (see Webhook Setup) |
NEXT_PUBLIC_CLERK_SIGN_IN_URL | No | Sign-in page path. Default: /login |
NEXT_PUBLIC_CLERK_SIGN_UP_URL | No | Sign-up page path. Default: /register |
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL | No | Redirect after sign-in. Default: /dashboard |
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL | No | Redirect after sign-up. Default: /dashboard |
The URL variables have sensible defaults already configured in Kit. You typically only need to set the three key variables:
bash
# apps/boilerplate/.env.local
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_your_key_here
CLERK_SECRET_KEY=sk_test_your_key_here
CLERK_WEBHOOK_SECRET=whsec_your_secret_here
ClerkProvider in Root Layout
Kit's root layout already includes the
ClerkProvider. You do not need to add it yourself — it is pre-configured and environment-aware.The provider uses the
dynamic prop, which is critical for Next.js 14 with streaming:typescript
<ClerkProvider
dynamic
appearance={{
elements: {
formButtonPrimary: 'bg-primary hover:bg-primary/90',
footerActionLink: 'text-primary hover:text-primary/90',
},
}}
>
{content}
</ClerkProvider>
Without
dynamic, Clerk bakes the session token into the server-rendered HTML. When Next.js streams the page, the client can receive a stale token, causing a hydration mismatch. The dynamic prop tells Clerk to read the session token on the client instead, avoiding the mismatch entirely.The
appearance object customizes Clerk's pre-built components (sign-in form, user button) to match your app's primary color. Since Kit uses CSS custom properties for theming, the bg-primary class automatically adapts to whichever color theme you have selected.Social Login Providers
Clerk makes it easy to add social logins. Kit's auth forms automatically display social login buttons when you enable providers in the Clerk Dashboard.
1
Create OAuth credentials
Go to the Google Cloud Console, create a new OAuth 2.0 Client ID, and set the authorized redirect URI to the value Clerk provides in the dashboard.
2
Enable in Clerk
In the Clerk Dashboard, go to User & Authentication > Social connections, enable Google, and paste your Client ID and Client Secret.
3
Test it
Restart your dev server. The sign-in and sign-up forms now show a "Continue with Google" button.
GitHub
1
Create an OAuth App
Go to GitHub Developer Settings, create a new OAuth App, and set the callback URL to the value Clerk provides.
2
Enable in Clerk
In the Clerk Dashboard, enable GitHub under Social connections and paste your Client ID and Client Secret.
3
Test it
Restart your dev server. The auth forms now include a "Continue with GitHub" button alongside any other providers you have enabled.
Clerk supports 20+ social providers including Apple, Microsoft, Discord, and more. Each one follows the same pattern: create OAuth credentials in the provider's dashboard, then enable and configure it in Clerk. See the Clerk Social Connections docs for the full list.
Multi-Factor Authentication
To enable MFA for your users:
- In the Clerk Dashboard, go to User & Authentication > Multi-factor.
- Enable Authenticator app (TOTP) and/or SMS verification.
- Users can then enable MFA from their profile settings.
Kit does not require any code changes — Clerk handles the entire MFA flow including setup, verification, and backup codes.
Webhook Setup
Webhooks are how Clerk notifies your application about user events. This is critical for keeping your database in sync — without webhooks, new users would have a Clerk account but no database record.
Create the Webhook Endpoint
1
Open Clerk Webhooks
In the Clerk Dashboard, go to Webhooks and click Add Endpoint.
2
Set the endpoint URL
For local development, use a tunneling service like ngrok or localtunnel:
bash
# Start your tunnel
ngrok http 3000
# Use the tunnel URL as your webhook endpoint:
# https://your-id.ngrok-free.app/api/webhooks/clerk
For production, use your actual domain:
https://yourdomain.com/api/webhooks/clerk
3
Select events
Subscribe to these events:
user.createduser.updateduser.deleted
4
Copy the signing secret
After creating the endpoint, Clerk shows a Signing Secret (starts with
whsec_). Copy this and add it to your apps/boilerplate/.env.local:bash
CLERK_WEBHOOK_SECRET=whsec_your_signing_secret_here
The Webhook Handler
Kit includes a pre-built webhook handler at
/api/webhooks/clerk. It receives Clerk events, verifies their authenticity, and updates your database. Here is the signature verification logic:src/app/api/webhooks/clerk/route.ts
export const POST = withRateLimit('webhooks', async (request: NextRequest) => {
try {
// Get the headers
const headerPayload = await headers()
const svix_id = headerPayload.get('svix-id')
const svix_timestamp = headerPayload.get('svix-timestamp')
const svix_signature = headerPayload.get('svix-signature')
// If there are no headers, error out
if (!svix_id || !svix_timestamp || !svix_signature) {
return new NextResponse('Error occured -- no svix headers', {
status: 400,
})
}
// Get the body
const payload = await request.json()
const body = JSON.stringify(payload)
// Create a new SVIX instance with your webhook secret
const wh = new Webhook(process.env.CLERK_WEBHOOK_SECRET || '')
let evt: WebhookEvent
// Verify the payload with the headers
try {
evt = wh.verify(body, {
'svix-id': svix_id,
'svix-timestamp': svix_timestamp,
'svix-signature': svix_signature,
}) as WebhookEvent
} catch (err) {
console.error('Error verifying webhook:', err)
return new NextResponse('Error occured', {
status: 400,
})
}
The handler is wrapped with
withRateLimit('webhooks', ...) to prevent abuse, and uses the Svix library to verify that each request genuinely comes from Clerk.SVIX Signature Verification
Every Clerk webhook request includes three headers:
| Header | Purpose |
|---|---|
svix-id | Unique message identifier (for deduplication) |
svix-timestamp | When the message was sent (prevents replay attacks) |
svix-signature | HMAC signature computed with your webhook secret |
The handler verifies these against the request body using your
CLERK_WEBHOOK_SECRET. If verification fails, the request is rejected with a 400 status.Handled Events
When a new user signs up, the
user.created event initializes their database record with a Free Tier:src/app/api/webhooks/clerk/route.ts
if (eventType === 'user.created') {
const { id, email_addresses, first_name, last_name } = evt.data
const email = email_addresses[0]?.email_address
const name = [first_name, last_name].filter(Boolean).join(' ') || null
// Initialize Free Tier credits
const freeCredits = parseInt(
process.env.NEXT_PUBLIC_CREDIT_FREE_TIER_CREDITS || '20'
)
const resetDate = new Date()
resetDate.setDate(resetDate.getDate() + 30) // 30 days from now
// Create user in database with Free Tier
await prisma.user.upsert({
where: { clerkId: id },
update: {
email,
name,
},
create: {
clerkId: id,
email,
name,
// Free Tier initialization
tier: 'free',
creditBalance: freeCredits,
creditsPerMonth: freeCredits,
creditsResetAt: resetDate,
bonusCredits: 0,
},
})
console.log(`User ${id} created with Free Tier (${freeCredits} credits)`)
}
Key details:
- Upsert operation — Uses
prisma.user.upsertinstead ofcreatefor idempotency. If Clerk retries the webhook, no duplicate records are created. - Free Tier credits — New users receive credits defined by
NEXT_PUBLIC_CREDIT_FREE_TIER_CREDITS(default: 500). - 30-day reset — The credit balance resets 30 days after account creation.
- Return 200 — Always returns a 200 status to acknowledge receipt. Returning errors causes Clerk to retry, which can create processing storms.
The
user.updated event syncs email and name changes, and user.deleted removes the record from the database.Development Without Clerk
If you have not set up Clerk yet, or you want to develop features without authentication overhead, Kit provides a no-auth development mode:
bash
pnpm dev:no-clerk
This sets
NEXT_PUBLIC_CLERK_ENABLED=false, which activates the mock authentication system:- A test user is automatically signed in (
free@test.example.com) - All dashboard routes are accessible without login
- Clerk SDK is not loaded (faster startup, no API calls)
- Auth hooks return mock data with identical interfaces
The test user matches the database seed data, so all features (credits, billing, AI chat) work as expected.
Demo mode (
NEXT_PUBLIC_DEMO_MODE=true) is similar but designed for public deployments. It shows a custom login form with tier-switching buttons so visitors can explore different subscription levels.