Clerk auth for Next.js
Use Clerk for the whole auth surface — session, sign-in/sign-up UI, and the user menu — and enforce protection in one place: the middleware. Pages and layouts may read auth state, but never gate on it.
Install & provider
npm install @clerk/nextjs
Wrap the root layout — Clerk sits above everything, including marketing pages,
so public pages can render <SignedIn> / <SignedOut> branches:
// app/layout.tsx
import { ClerkProvider } from "@clerk/nextjs";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<ClerkProvider>
<html lang="en">
<body>{children}</body>
</html>
</ClerkProvider>
);
}
Middleware — the single gate
Protect the dashboard subtree by path prefix. Everything else stays public:
// middleware.ts
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
const isProtected = createRouteMatcher(["/dashboard(.*)"]);
export default clerkMiddleware(async (auth, req) => {
if (isProtected(req)) await auth.protect();
});
export const config = {
matcher: ["/((?!_next|.*\\..*).*)", "/(api|trpc)(.*)"],
};
auth.protect() redirects signed-out visitors to sign-in and returns them to
the page they wanted after authenticating.
Sign-in / sign-up pages
Use Clerk's prebuilt components on catch-all routes:
app/sign-in/[[...sign-in]]/page.tsx → <SignIn />
app/sign-up/[[...sign-up]]/page.tsx → <SignUp />
Point Clerk at them in .env.local:
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_…
CLERK_SECRET_KEY=sk_test_…
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL=/dashboard
NEXT_PUBLIC_CLERK_SIGN_UP_FALLBACK_REDIRECT_URL=/dashboard
After sign-in, land users in the dashboard — not back on the marketing page.
Reading auth state
- Public pages / nav:
<SignedIn>/<SignedOut>for the CTA swap, and<UserButton />in the dashboard header for the account menu. - Server Components & server actions:
const { userId } = await auth()from@clerk/nextjs/server. In server actions, treat a missinguserIdas an error even though middleware should have caught it — actions are an API surface of their own. - Store
userId(the Clerk ID) as the owner column on your own tables — seedrizzle-postgres-docker. Don't mirror Clerk's user profile into Postgres unless you need to query it.