Clerk Authentication
User authentication and session management with Clerk
Clerk Authentication
SavvySolve uses Clerk for authentication, providing a complete user management solution without building custom auth infrastructure. Clerk handles sign-up, sign-in, session management, and user profiles, allowing the team to focus on core product features.
Why Clerk?
Authentication is a security-critical component that's easy to get wrong. Rolling custom auth means handling password hashing, session management, email verification, password reset flows, social login integrations, and security updates. Clerk handles all of this out of the box.
For SavvySolve specifically, Clerk's value proposition aligns well with the target users:
- Seniors (65+) benefit from familiar sign-in flows and social login options
- Busy professionals appreciate quick sign-in without friction
- Small business owners may already use Google Workspace, making Google sign-in seamless
Clerk's pricing (free up to 10,000 monthly active users) makes it cost-effective for the MVP phase while providing enterprise-grade security.
Architecture Overview
The Clerk integration has four main components:
Note: Next.js 16+ uses
proxy.tsinstead ofmiddleware.ts. The functionality is identical, but the file convention has changed.
Provider Setup
The ClerkProvider wraps the entire application in app/layout.tsx, making authentication state available throughout the component tree:
import { ClerkProvider } from "@clerk/nextjs";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<ClerkProvider>
<html lang="en" suppressHydrationWarning>
<body>
<TRPCProvider>
<RootProvider>{children}</RootProvider>
</TRPCProvider>
</body>
</html>
</ClerkProvider>
);
}The provider order matters: ClerkProvider wraps everything because authentication state must be available before any other providers or components render.
Route Protection
The proxy intercepts requests before they reach route handlers, enforcing authentication where required:
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
const isPublicRoute = createRouteMatcher([
"/",
"/sign-in(.*)",
"/sign-up(.*)",
"/api/webhooks(.*)",
"/api/trpc(.*)",
"/docs(.*)",
]);
export default clerkMiddleware(async (auth, req) => {
if (!isPublicRoute(req)) {
await auth.protect();
}
});Public routes include the landing page, authentication pages, webhooks (which use their own verification), the tRPC API (which has its own auth checks via procedures), and documentation.
Protected routes automatically redirect unauthenticated users to the sign-in page. After signing in, users return to their original destination.
tRPC Integration
The tRPC context exposes authentication state to all procedures, enabling both public and protected endpoints:
import { initTRPC, TRPCError } from "@trpc/server";
import { auth } from "@clerk/nextjs/server";
interface AuthState {
userId: string | null;
sessionId: string | null;
}
export interface Context {
auth: AuthState;
}
export async function createContext(): Promise<Context> {
const { userId, sessionId } = await auth();
return {
auth: { userId, sessionId },
};
}Two procedure types handle different authorization needs:
// Public procedure - anyone can call
export const publicProcedure = t.procedure;
// Protected procedure - requires authentication
const enforceAuth = t.middleware(async ({ ctx, next }) => {
if (!ctx.auth.userId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You must be signed in to access this resource",
});
}
return next({ ctx });
});
export const protectedProcedure = t.procedure.use(enforceAuth);Use publicProcedure for endpoints like health checks or public ticket submission. Use protectedProcedure for solver dashboards, session management, and anything requiring a user identity.
Webhook User Sync
When users sign up or update their profiles in Clerk, webhooks notify SavvySolve to keep the local database synchronized:
import { Webhook } from "svix";
import type { WebhookEvent } from "@clerk/nextjs/server";
import {
extractUserData,
syncUserToDatabase,
handleUserDeletion,
} from "@/lib/services/user-sync";
export async function POST(req: Request) {
// Verify webhook signature
const wh = new Webhook(process.env.CLERK_WEBHOOK_SECRET!);
const evt = wh.verify(body, headers) as WebhookEvent;
// Handle events
switch (evt.type) {
case "user.created":
case "user.updated":
const userData = extractUserData(evt.data);
await syncUserToDatabase(userData);
break;
case "user.deleted":
await handleUserDeletion(evt.data.id);
break;
}
}The user sync service extracts relevant fields from Clerk's user object and persists them locally:
export interface ClerkUserData {
clerkId: string;
email: string | null;
firstName: string | null;
lastName: string | null;
imageUrl: string | null;
}
export function extractUserData(clerkUser: UserJSON): ClerkUserData {
const primaryEmail = clerkUser.email_addresses.find(
(email) => email.id === clerkUser.primary_email_address_id
);
return {
clerkId: clerkUser.id,
email: primaryEmail?.email_address ?? null,
firstName: clerkUser.first_name,
lastName: clerkUser.last_name,
imageUrl: clerkUser.image_url,
};
}Webhook handlers use upsert logic for idempotency—Clerk may retry webhooks, and the handler should produce the same result regardless of how many times it runs.
Environment Variables
The integration requires three environment variables:
| Variable | Description |
|---|---|
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY | Public key for client-side SDK (starts with pk_) |
CLERK_SECRET_KEY | Secret key for server-side operations (starts with sk_) |
CLERK_WEBHOOK_SECRET | Webhook signing secret for signature verification (starts with whsec_) |
Get these from the Clerk Dashboard:
- Keys are under API Keys
- Webhook secret is created when adding a webhook endpoint under Webhooks
Setting Up Webhooks
To enable user sync:
- Go to Webhooks in the Clerk Dashboard
- Add endpoint:
https://your-domain.com/api/webhooks/clerk - Subscribe to events:
user.created,user.updated,user.deleted - Copy the signing secret to
CLERK_WEBHOOK_SECRET
For local development, use a tunnel service like ngrok to expose your local server.
Using Auth in Components
Client components can access auth state through Clerk's hooks:
"use client";
import { useUser, SignInButton, UserButton } from "@clerk/nextjs";
export function Header() {
const { isSignedIn, user } = useUser();
return (
<header>
{isSignedIn ? (
<>
<span>Welcome, {user.firstName}</span>
<UserButton />
</>
) : (
<SignInButton />
)}
</header>
);
}Related Documentation
- API Layer - tRPC procedures using auth context
- Architecture Overview - System design decisions