Building with AI · 11 min read

Clerk auth in a weekend

What Clerk is, when it's the right call, the happy path for a Next.js app, and the three mistakes everyone makes.

Auth is the part of building an app that everyone underestimates once and never again. Password hashing, session tokens, email verification, password reset flows, social login, the occasional security patch at midnight: it is a lot of surface area for a feature users only notice when it breaks. Clerk exists to take that surface area off your plate, and for most apps built by a small team, it is worth handing over.

Clerk is a hosted service that handles sign up, sign in, sessions, and user profiles, with drop-in components for React and Next.js so you are not building forms from scratch. It also has a strong idea of “organizations,” which matters if your app will ever have teams, clients, or accounts with more than one person in them. We use it for our own client portal for exactly that reason.

What you get out of the box

The reason a weekend is realistic at all is how much ships already built. A handful of features that would each take real engineering time to write from scratch come free with a working Clerk account:

  • Prebuilt sign-in, sign-up, and account management forms that already handle validation, error states, and loading states.
  • Social login through Google, GitHub, and the other common providers, configured through the dashboard rather than by writing OAuth handshakes yourself.
  • Session management and token refresh handled automatically, so you never write the code that decides when a login has expired.
  • Organizations, invitations, and roles, ready to turn on the moment your app needs more than one person per account.

None of that is exotic; it is the boring, essential plumbing every serious app eventually needs. Getting it for the price of an SDK install, rather than a sprint of engineering time, is the actual trade you are making.

When it is the right call

Reach for Clerk when you want auth solved rather than owned. If your app is a normal product with sign in, sign up, and maybe team accounts, and you would rather spend your engineering time on the thing that makes your product different, Clerk is a sound default. It is also the right call the moment you need organizations: multiple users under one account, with roles and invitations, is exactly the feature Clerk was built around, and building it yourself is a multi-week project even for an experienced team.

Skip it, or at least slow down, if your auth requirements are unusual: a highly custom login flow, strict data residency rules that conflict with a hosted provider, or a login system that has to live entirely inside infrastructure you already run. Those are real constraints for regulated industries and larger enterprises, and they are worth naming honestly rather than discovering three weeks into a build. Clerk also charges per monthly active user past a free tier, so it is worth modeling that cost if you expect a large free user base early. For the ordinary case, a Next.js app that needs real accounts working this weekend, it is the fastest path there by a wide margin.

The happy path for a Next.js app

The setup is small enough to do in one sitting. Create a Clerk account, make an application, and grab the two API keys it gives you. Install the SDK, drop the keys into your environment file, and wrap your app in the provider:

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>
  );
}

Next, add a middleware file so Clerk knows which routes need a signed-in user and which are public. The components handle the forms; this file is what actually protects your app:

middleware.ts
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";

const isProtectedRoute = createRouteMatcher(["/dashboard(.*)"]);

export default clerkMiddleware(async (auth, req) => {
  if (isProtectedRoute(req)) {
    await auth.protect();
  }
});

export const config = {
  matcher: ["/((?!_next|.*\\..*).*)", "/(api|trpc)(.*)"],
};

Drop Clerk's prebuilt <SignIn /> and <SignUp /> components on their own routes, add the user button to your header, and you have working authentication with almost no custom UI written. That is a genuine weekend project, most of it spent on your own app rather than on the auth itself.

Wiring it to data you actually own

Clerk stores the account itself: email, password, session, profile photo. Your own database still needs a row for that user the moment they exist, so the rest of your app has somewhere to hang orders, preferences, or whatever your product actually tracks. The clean way to do that is a webhook: Clerk fires an event every time a user signs up, updates their profile, or gets deleted, and you catch that event on your own server to keep your database in sync.

Inside your app, two small helpers cover almost every case you will hit. Server-side, auth() tells you who is signed in and lets you protect a server action or route handler in one line. Client-side, useUser() gives a component the current user without a network call of its own, since Clerk keeps the session cached in the browser. Reach for those two before building anything custom; they cover the large majority of what a typical app needs from auth day to day.

Before you call the setup finished, actually test it as two different users. Create a second test account, put it in a second organization if you are using those, and confirm one account genuinely cannot see the other's data through the UI or by calling the API directly. That last part matters more than it sounds: a route that checks who is signed in but not which organization they belong to will happily hand back another customer's records to anyone who is merely logged in.

The three mistakes everyone makes

  • Protecting the page and forgetting the API route behind it. Hiding a dashboard link from a signed-out user does nothing if the API endpoint that dashboard calls has no auth check of its own. Every server-side route that touches real data needs its own auth.protect() or equivalent, independent of what the UI shows.
  • Mixing development and production keys, or hardcoding a redirect URL that only works locally. Clerk gives you separate keys per environment on purpose; keep them in environment variables per deployment, never checked into the repo, and never assume the redirect that worked on localhost will work once you have a real domain.
  • Bolting organizations on after the fact. If there is any chance your app will need team accounts, clients, or shared workspaces later, model that from the start, even if only one person is on each organization today. Retrofitting multi-user accounts onto an app built for single users is a much bigger job than building it in from day one.

The pattern underneath all three is the same: the UI hiding something is not the same as the server refusing it, and an environment mismatch is invisible until it ships. Fifteen minutes checking both closes most of the gap between a demo and something you would actually trust with real user data.

None of the three are exotic bugs you would need years of security experience to catch. They are checklist items, the kind a second reviewer catches on a first read of the code, and the kind an agent can be told to check for explicitly before it calls an auth setup finished. Writing them down once, the way we just did here, is most of the work of avoiding them.

We wire Clerk into every product we build that needs real accounts, our own client portal included. If you want it set up correctly the first time, that is part of our AI consulting work, and the membership includes the deeper setup playbook.