TL;DR: Sign up at clerk.com, create an application, install @clerk/nextjs (or your framework's package), wrap the app in <ClerkProvider>, drop in <SignIn /> and <UserButton />. Free tier: 10,000 MAUs.
Drop in <SignIn /> and you have a working auth UI. Clerk handles passwords, OAuth, MFA, sessions, organizations β you don't write any of it. Free tier covers 10K monthly active users.
Clerk's sweet spot: fast-moving SaaS startups that want auth done well without thinking about it.
Sign up at dashboard.clerk.com β. Free tier: 10,000 monthly active users, no credit card needed.
Create an application. Pick which sign-in methods to enable:
Dashboard β API Keys. Copy two values:
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxxxx
CLERK_SECRET_KEY=sk_test_xxxxx
Publishable is safe to ship to the browser; secret is backend-only.
Next.js App Router (most common):
npm install @clerk/nextjs
In middleware.ts at the project root:
import { clerkMiddleware } from '@clerk/nextjs/server';
export default clerkMiddleware();
export const config = {
matcher: ['/((?!_next|.*\\..*).*)', '/api/(.*)'],
};
In app/layout.tsx:
import { ClerkProvider } from '@clerk/nextjs';
export default function RootLayout({ children }) {
return (
<ClerkProvider>
<html><body>{children}</body></html>
</ClerkProvider>
);
}
Done. Auth is now wired through your whole app.
Same pattern (provider component + middleware/server helpers) for:
Clerk ships pre-built components. In Next.js:
// app/sign-in/[[...sign-in]]/page.tsx
import { SignIn } from '@clerk/nextjs';
export default function Page() {
return <SignIn />;
}
That's the entire sign-in page. Polished UI with whatever providers you enabled in Step 1. Same shape for <SignUp />, <UserButton /> (avatar dropdown with account management), and <OrganizationSwitcher />.
// app/dashboard/page.tsx
import { auth } from '@clerk/nextjs/server';
import { redirect } from 'next/navigation';
export default async function Dashboard() {
const { userId } = await auth();
if (!userId) redirect('/sign-in');
return <div>Welcome to your dashboard</div>;
}
Or use middleware to protect entire route groups β see clerkMiddleware docs β for the matcher patterns.
// app/api/me/route.ts
import { auth, currentUser } from '@clerk/nextjs/server';
export async function GET() {
const { userId } = await auth();
if (!userId) return new Response('Unauthorized', { status: 401 });
const user = await currentUser();
return Response.json({
email: user.emailAddresses[0].emailAddress,
name: user.firstName,
});
}
For non-Next frameworks: use JWT verification β from the official Backend SDK.
Clerk components ship looking generic-but-clean. To match your brand:
<ClerkProvider appearance={{
baseTheme: undefined,
variables: {
colorPrimary: '#6366f1',
colorBackground: '#ffffff',
borderRadius: '8px',
fontFamily: '"DM Sans", system-ui, sans-serif',
},
}}>
...
</ClerkProvider>
For deeper customization (your own components), use Clerk's Elements primitives β β unstyled headless components you wrap with your own design system.
If your product is B2B (each user belongs to one or more companies), enable Organizations in the dashboard:
Configure β Organizations β Enable. Then add <OrganizationSwitcher /> to your UI β gives users a switcher dropdown to flip between orgs they belong to.
Server-side, the current org context is on auth():
const { userId, orgId, orgRole } = await auth();
Built-in roles: admin, basic_member. Add custom permissions in the dashboard. Organizations docs β.
Clerk holds the user records. Your database holds your app data. To link them, listen for Clerk webhooks:
Clerk dashboard β Webhooks β New endpoint. Pick events (user.created, user.updated, user.deleted). Get the signing secret.
// app/api/webhooks/clerk/route.ts
import { Webhook } from 'svix';
export async function POST(req) {
const payload = await req.text();
const headers = Object.fromEntries(req.headers);
const wh = new Webhook(process.env.CLERK_WEBHOOK_SECRET);
const event = wh.verify(payload, headers);
if (event.type === 'user.created') {
await db.users.insert({
clerk_id: event.data.id,
email: event.data.email_addresses[0].email_address,
});
}
return new Response('ok');
}
Same signature-verify pattern as Stripe webhooks. Clerk webhooks docs β.
If you already have a user table and want to import: use the migration guides β. Bulk-import API accepts hashed passwords from common formats (bcrypt, argon2, PBKDF2).
If you're worried about vendor lock-in: Clerk exports all users + sessions on demand. Migrating out is annoying but possible.
auth.yourdomain.com instead of your-app.clerk.accounts.dev. Free; one DNS record.