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.
Features#
- Middleware-level route protection. Everything is private by default; the public surface is declared, not the private one.
- Hosted sign-in and sign-up mounted as Clerk components rather than
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 (App Router) |
| 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 providerHow the auth wiring works#
The entire protection story is the middleware:
import { authMiddleware } from '@clerk/nextjs'
export default authMiddleware({
publicRoutes: ['/api/:path*'],
})
export const config = {
matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
}Two things worth noticing:
- Deny by default. Every route the matcher covers requires a session unless
it is named in
publicRoutes. The inverse — listing what to protect — is how a new page ends up shipping unauthenticated because someone forgot 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 at all.
Running locally#
git clone https://github.com/Vette1123/next13-auth.git
cd next13-auth
npm installCreate .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 stringnpm run devKeys come from the Clerk dashboard; the app will not boot without them, by
design — @t3-oss/env-nextjs validates the environment at startup rather than
letting a missing key surface as a runtime error later.
Notes#
The dashboard UI started from the shadcn/ui example and was kept as the protected surface — the point of the project was the auth and routing wiring around it, not the chart data, which is static.
Built by Mohamed Gado · 2023