Phase 6: Adding Auth
With Clerk, adding auth to a personal project takes about twenty minutes. Passkeys, social login, MFA, and password reset come for free.
Phase 6: Adding Auth
In one line: Don't build auth. Use Clerk (or Better Auth) and spend the saved month on the actual product.
Authentication looks easy. "It's just an email and a password, right?" Then come password resets, email verification, social logins, multi-factor, session management, account recovery, breach notifications, passkeys, OAuth flows, and the half-dozen subtle attacks each defends against. Services like Clerk have spent millions of dollars getting this right. Use them.
Twenty minutes to working auth
With Clerk, auth takes 20 minutes. Two terms before the code: middleware (a function Next.js runs on every incoming request before your page handler) and passkeys (a phishing-resistant alternative to passwords that uses public-key cryptography built into the browser and OS).
// src/middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
const isProtected = createRouteMatcher(['/library(.*)', '/settings(.*)']);
export default clerkMiddleware(async (auth, req) => {
if (isProtected(req)) {
await auth.protect();
}
});
export const config = {
matcher: ['/((?!_next|.*\\..*).*)'],
};
In English: This runs on every page request.
createRouteMatcherbuilds a matcher for paths under/libraryand/settings. If the incoming request is for one of those,auth.protect()redirects unauthenticated users to the sign-in page; otherwise the request passes through unchanged. Theconfig.matcherregex excludes static asset URLs so middleware doesn't fire on every PNG and CSS file.
// src/app/layout.tsx
import { ClerkProvider, SignedIn, SignedOut, SignInButton, UserButton } from '@clerk/nextjs';
export default function RootLayout({ children }) {
return (
<ClerkProvider>
<html lang="en">
<body>
<nav className="border-b p-4 flex justify-between items-center">
<a href="/" className="font-bold">ShelfTrack</a>
<SignedIn>
<UserButton />
</SignedIn>
<SignedOut>
<SignInButton />
</SignedOut>
</nav>
{children}
</body>
</html>
</ClerkProvider>
);
}
In English:
<ClerkProvider>wraps the whole app so any child component can ask "is the user signed in, and who are they?"<SignedIn>and<SignedOut>are conditional render helpers that show their children only in the matching auth state.<UserButton>is a pre-built avatar dropdown with "Manage Account / Sign out";<SignInButton>opens the hosted sign-in modal. You wrote zero auth UI.
Done. Users can sign up, sign in, manage their account, sign out. Clerk handles passkeys, social login, multi-factor, password reset — all of it.
After you wire up the middleware and <ClerkProvider>:
- Visit a protected route while signed out → you get redirected to a sign-in page Clerk renders for you.
- Sign up with a brand-new email → check your inbox; the verification email is already styled.
- Hit the
<UserButton />dropdown → "Manage Account" gives users a full settings panel (avatar, password, MFA, connected accounts) you didn't build. - Turn on Google login in the Clerk dashboard → "Sign in with Google" appears on the sign-in page with zero code change.
Each of those would be a one-to-three-day task to build yourself. You just got them all for $0 on the free tier.
Clerk's free tier covers up to 10,000 monthly active users. The cost isn't "renting a login form" — it's renting:
- Up-to-date OAuth integrations with Google, GitHub, Apple, Microsoft, et al.
- Passkey support that actually works on iOS/Android/Chrome.
- Account-takeover protection and breached-password checks.
- A hosted, accessible sign-in UI you don't have to design or maintain.
- Compliance posture (SOC 2, GDPR data deletion) you'd otherwise need to build yourself.
At $0 for the first 10K users, you'd be irrational to roll your own.
Common mistakes
- Re-checking auth only in middleware. Middleware redirects unauthenticated users, but it doesn't stop a Server Action from running if someone POSTs to it directly. The fix is to call
await auth()and verifyuserIdinside every Server Action and Route Handler too — defense in depth, not "the middleware will catch it." - Using the Clerk user ID as the database primary key directly. It works until you migrate auth providers and discover every foreign key in your schema points at strings only Clerk understands. The fix is to store
clerkUserIdas atextcolumn alongside your ownusersrow, and key foreign relationships off your own ID. - Hand-rolling a "just one thing" auth feature. "Clerk handles everything but I want my own magic-link flow" — six weekends later you've built half an auth system. The fix is to live with Clerk's defaults for v1; if you genuinely need something Clerk doesn't do, that's the signal to switch to Better Auth, not to start patching.
- Hard-coding the user ID in dev to "skip auth for now." It saves five minutes, then becomes a
userId === "tony-dev"check that ships to production. The fix is to wire Clerk's dev keys on day one — sign-in takes two clicks locally and you never have to remove a stub later.
Page checkpoint
Did the auth choices stick?
RequiredWhat's next
→ Continue to Phase 7: Payments where Stripe Checkout + a webhook handle the entire money flow.