Projects

Next.js Authentication Using Clerk

2023·3 min read

React
TypeScript
Tailwind CSS
Next.js
Clerk
Link (opens in a new tab)GitHub (opens in a new tab)

A Next.js 13 App Router application with Clerk in front of a full analytics dashboard — stat cards, a revenue chart, a recent-activity list, a team switcher, a command-palette search and a date-range picker, all behind a login.

On this page6 sections
  • Features
  • Stack
  • Route structure
  • How the auth wiring works
  • Running locally
  • Notes

A Next.js 13 App Router application with Clerk in front of a full analytics dashboard — stat cards, a revenue chart, a recent-activity list, a team switcher, a command-palette search and a date-range picker, all behind a login.

The interesting part is that essentially none of the auth is written by hand. Route protection is a middleware matcher, not a wrapper around each page, and it is declared deny-by-default: everything the matcher covers requires a session unless it is explicitly named in publicRoutes. The inverse arrangement — listing what to protect — is how a new page ships unauthenticated because somebody forgot to add it to a list.

Because the check happens in middleware, an unauthenticated request never reaches a page at all. There is no session loading state, no client-side redirect, and no flash of the wrong UI while the user resolves. The matcher also deliberately skips _next and anything with a file extension, since running auth on every chunk and image costs real latency for nothing.

Route groups keep the two shells apart without appearing in the URL: (auth) renders login and register chrome-free, (dashboard) carries the nav and user menu.

Built on Clerk, PlanetScale's serverless driver, shadcn/ui over Radix, Recharts, and react-hook-form with Zod resolvers.

A Next.js 13 App Router application with Clerk handling authentication in front of a full analytics dashboard: route groups, protected pages, hosted sign-in and sign-up, and a user profile screen.

Live demo →

Features#

  • Middleware-level route protection. Everything is private by default, and only the public routes are declared.
  • Hosted sign-in and sign-up mounted as Clerk components instead of hand-built forms, themed through @clerk/themes.
  • Dashboard with stat cards, a revenue bar chart, a recent-activity list, tabs, a team switcher and a command-palette search.
  • Date range picker for scoping the dashboard view.
  • Profile page under the authenticated layout.
  • Light and dark themes via next-themes, no flash on first paint.

Stack#

Framework Next.js 13.4.7 (App Router), React 18.2
Auth Clerk (@clerk/nextjs, @clerk/themes)
Database PlanetScale serverless driver
UI shadcn/ui on Radix primitives + Tailwind CSS
Charts Recharts
Forms react-hook-form + Zod via @hookform/resolvers
Misc cmdk (command menu), sonner (toasts), react-day-picker, date-fns
Env safety @t3-oss/env-nextjs

Route structure#

Route groups keep the two layouts apart without leaking into the URL:

app/
├── (auth)/
│   ├── layout.tsx          # centred, chrome-free
│   └── (routes)/
│       ├── login/
│       └── register/
├── (dashboard)/
│   ├── layout.tsx          # nav, user menu, theme toggle
│   ├── page.tsx            # the dashboard
│   └── profile/
└── layout.tsx              # ClerkProvider + theme provider

How the auth wiring works#

All route protection lives in the middleware:

import { authMiddleware } from '@clerk/nextjs'
 
export default authMiddleware({
  publicRoutes: ['/api/:path*'],
})
 
export const config = {
  matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
}

Two details:

  • Deny by default. Every route the matcher covers requires a session unless it is named in publicRoutes. Listing the protected routes instead lets a new page ship unauthenticated if someone forgets to add it.
  • The matcher deliberately skips static assets and _next. Running auth middleware on every image and chunk is a measurable cost for no benefit.

Because the check happens in middleware, the dashboard pages never render a loading state for the session, and there is no client-side redirect flicker. An unauthenticated request never reaches the page.

This is the 2023 API. authMiddleware was replaced by clerkMiddleware() in @clerk/nextjs v5, Next.js 16 renamed middleware.ts to proxy.ts, and Clerk now recommends await auth.protect() in each page instead of path checks in middleware. The port is written up in Clerk auth in Next.js 16: proxy.ts and auth.protect().

Running locally#

git clone https://github.com/Vette1123/next13-auth.git
cd next13-auth
yarn install

Create .env.local:

NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/login
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/register
DATABASE_URL=mysql://...   # PlanetScale connection string
yarn dev

Keys come from the Clerk dashboard. The app will not boot without them: @t3-oss/env-nextjs validates the environment at startup, so a missing key does not surface as a runtime error later.

Notes#

The dashboard UI started from the shadcn/ui example and was kept as the protected surface. The project is about the auth and routing wiring around it. The chart data is static.


Built by Mohamed Gado · 2023

Writing about this project

  • Clerk auth in Next.js: from authMiddleware to proxy.ts and auth.protect()

More projects

  • Simple Time Logger2023 · React
  • Custom Next.js Template2023 · React
  • Real time chat app2023 · React
All projects
  • Features
  • Stack
  • Route structure
  • How the auth wiring works
  • Running locally
  • Notes