# When to use these instructions

Use this skill whenever the user is adding login/signup, sessions, social login, password reset, or anything related to user accounts. Pick the right managed auth provider and walk them through the wiring step by step.

---

# Authentication

The topic beginners are most afraid of and most likely to get wrong. The
fix is to never roll your own — pick a managed auth provider and follow
its docs exactly.

## When to use this
- The user wants to add login, signup, or user accounts.
- The user is hitting "how do I know who's logged in" problems.
- The user has built their own auth and wants to migrate to something safer.
- The user is adding social login (Google, GitHub, Apple, etc.).

## Stack-aware

This skill works for two common auth paths:
- **Stack A (Clerk):** Best DX, drop-in components, generous free tier. Best when you want to ship in a day and don't mind a paid tier later.
- **Stack B (Supabase Auth):** Free, includes the database, works with Render, OAuth-only signups are easy. Best when Supabase is already your backend OR you want zero per-user fees.

Both are production-grade. Stack A is faster to wire up; Stack B is cheaper at scale and more flexible for indie projects. Both playbooks below.

## Pick the right provider

Match the provider to the stack — there's a clear right answer per stack:

| Stack | Recommended | Why |
|---|---|---|
| **Next.js (App Router)** | **Clerk** | Best DX, drop-in components, generous free tier |
| Next.js (alt) | Better Auth | Self-hosted, owns the database, growing fast |
| Next.js (alt) | Auth.js (NextAuth v5) | Mature, free, more setup |
| Vue / Nuxt | Supabase Auth | Tightly integrated, easy |
| SvelteKit | Lucia or Supabase Auth | Both work well |
| Plain React | Clerk or Supabase Auth | Both have React SDKs |
| Mobile (Expo) | Clerk or Supabase Auth | Both have native SDKs |
| Custom backend | Better Auth or Lucia | Library-based, you own the storage |

**Never roll your own** for a beginner project. Password hashing, session
tokens, CSRF, password reset flows, account recovery, MFA, suspicious
activity detection — all of these are landmines. Managed providers handle
them all for free.

## The Clerk + Next.js playbook (recommended path)

### Step 1 — Install
```bash
npm install @clerk/nextjs
```

### Step 2 — Add env vars
Get these from clerk.com → create an app → API keys.
```
# .env.local
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...
```

### Step 3 — Wrap the app
```tsx
// app/layout.tsx
import { ClerkProvider } from "@clerk/nextjs";

export default function RootLayout({ children }) {
  return (
    <ClerkProvider>
      <html>
        <body>{children}</body>
      </html>
    </ClerkProvider>
  );
}
```

### Step 4 — Add middleware
```ts
// 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: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"],
};
```

### Step 5 — Sign-in / sign-up pages
```tsx
// app/sign-in/[[...sign-in]]/page.tsx
import { SignIn } from "@clerk/nextjs";
export default function Page() {
  return <SignIn />;
}
```
```tsx
// app/sign-up/[[...sign-up]]/page.tsx
import { SignUp } from "@clerk/nextjs";
export default function Page() {
  return <SignUp />;
}
```

### Step 6 — Show the user button in the navbar
```tsx
import { SignedIn, SignedOut, SignInButton, UserButton } from "@clerk/nextjs";

<header>
  <SignedOut>
    <SignInButton />
  </SignedOut>
  <SignedIn>
    <UserButton />
  </SignedIn>
</header>
```

### Step 7 — Read the user in a server component
```tsx
// app/dashboard/page.tsx
import { currentUser } from "@clerk/nextjs/server";

export default async function Dashboard() {
  const user = await currentUser();
  if (!user) return null; // middleware already protects this
  return <h1>Welcome, {user.firstName}</h1>;
}
```

That's the entire setup. Social login (Google, GitHub, etc.) is enabled in
the Clerk dashboard with a single toggle — no code changes needed.

## The Supabase Auth playbook (for non-Clerk projects)

### Step 1 — Install
```bash
npm install @supabase/supabase-js @supabase/ssr
```

### Step 2 — Env vars
```
NEXT_PUBLIC_SUPABASE_URL=https://...supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=ey...
```

### Step 3 — Create the clients
Follow the @supabase/ssr docs exactly — there's a browser client, a server
client, and a middleware client. Put them in `src/lib/supabase/`.

### Step 4 — Sign-in page
```tsx
"use client";
import { createBrowserClient } from "@supabase/ssr";

export function LoginForm() {
  const supabase = createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  );

  async function handleLogin(e) {
    e.preventDefault();
    const { error } = await supabase.auth.signInWithPassword({
      email: e.target.email.value,
      password: e.target.password.value,
    });
    if (error) console.error(error);
    else window.location.href = "/dashboard";
  }

  return (
    <form onSubmit={handleLogin}>
      <input name="email" type="email" required />
      <input name="password" type="password" required />
      <button>Sign in</button>
    </form>
  );
}
```

### Step 5 — Read the user
```tsx
// In a server component
import { createServerClient } from "@/lib/supabase/server";

export default async function Dashboard() {
  const supabase = await createServerClient();
  const { data: { user } } = await supabase.auth.getUser();
  if (!user) redirect("/login");
  return <h1>Welcome, {user.email}</h1>;
}
```

## Social login

With Clerk: enable each provider in the dashboard. Done.

With Supabase: enable each provider in the dashboard, then add a button:
```tsx
<button onClick={() => supabase.auth.signInWithOAuth({ provider: "google" })}>
  Sign in with Google
</button>
```

You'll also need an auth callback route:
```ts
// app/auth/callback/route.ts
import { createServerClient } from "@/lib/supabase/server";
import { NextResponse } from "next/server";

export async function GET(req: Request) {
  const { searchParams, origin } = new URL(req.url);
  const code = searchParams.get("code");
  if (code) {
    const supabase = await createServerClient();
    await supabase.auth.exchangeCodeForSession(code);
  }
  return NextResponse.redirect(`${origin}/dashboard`);
}
```

## OAuth-only signup (no email/password) [Stack B]

For most indie projects, **email/password is more trouble than it's worth**:
you have to handle password reset flows, account recovery, email verification,
spam signups, and the entire "I forgot my password" support burden.

The simpler path: **only allow Google OAuth** (or Google + GitHub). No
password ever exists. Account recovery is "click Sign in with Google again".

```tsx
// app/login/page.tsx
"use client";
import { createBrowserClient } from "@supabase/ssr";

export default function LoginPage() {
  const supabase = createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
  );

  async function signInWithGoogle() {
    await supabase.auth.signInWithOAuth({
      provider: "google",
      options: {
        redirectTo: `${window.location.origin}/auth/callback`,
      },
    });
  }

  return (
    <div className="max-w-md mx-auto py-20 text-center">
      <h1 className="text-3xl font-bold mb-4">Sign in</h1>
      <p className="text-muted mb-8">
        We use Google to sign you in. No password to remember.
      </p>
      <button
        onClick={signInWithGoogle}
        className="inline-flex items-center gap-3 px-6 py-3 rounded-xl border border-border bg-surface hover:bg-surface-alt font-semibold"
      >
        <svg className="w-5 h-5" viewBox="0 0 24 24">{/* Google icon */}</svg>
        Continue with Google
      </button>
    </div>
  );
}
```

**Steps to enable in Supabase:**
1. Supabase dashboard → Authentication → Providers → Google → Enable
2. Add your Client ID + Secret from console.cloud.google.com
3. In Google Cloud Console, add your prod domain to **Authorized redirect URIs**:
   `https://yoursite.com/auth/callback` AND `https://<project>.supabase.co/auth/v1/callback`
4. Done. No email templates, no password rules, no recovery flow to build.

**The whole signup-and-onboarding pattern** for OAuth-only sites:
1. User clicks "Sign in with Google" → bounces through Google → lands on `/auth/callback`
2. Callback exchanges the code for a session, creates a Supabase user
3. Trigger or server-side check: if first time, insert a row into `user_profiles` with empty username
4. Redirect to `/onboarding` if profile is incomplete (no username yet), else to `/dashboard`
5. Onboarding form collects username, avatar, optional fields → updates the profile row → redirects to dashboard

This is the chokchok pattern (skin type instead of username) and it works
beautifully — minimal friction, no password support tickets, and you get
real users instead of throwaway emails.

## Email-allowlist admin auth [Stack B]

For internal admin pages (`/admin`), the cheapest pattern is **Google OAuth
+ a hardcoded allowlist of admin emails**. No roles table, no permissions
system, no extra UI. Five lines of code.

```tsx
// app/admin/layout.tsx
import { redirect } from "next/navigation";
import { createServerClient } from "@/lib/supabase/server";

const ADMIN_EMAILS = new Set([
  "you@yoursite.com",
  "cofounder@yoursite.com",
]);

export default async function AdminLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const supabase = await createServerClient();
  const {
    data: { user },
  } = await supabase.auth.getUser();

  if (!user) redirect("/login?next=/admin");
  if (!ADMIN_EMAILS.has(user.email ?? "")) redirect("/");

  return <>{children}</>;
}
```

**Why this is fine:**
- The allowlist lives in code, not the database — it's version-controlled and tamper-proof
- It works for 1-10 admins (a real CMS pattern only matters at 50+ admins)
- Adding a new admin = a one-line PR + redeploy
- No "permission denied" UI needed — non-admins are silently redirected
- Combine with RLS policies on Supabase tables that check `auth.email() = ANY(ARRAY[...])` for an extra layer

**Once you outgrow it** (more than ~10 admins, or you need granular roles):
move the allowlist to a `user_roles` table and check role membership instead.
The transition is 30 lines of code.

## What good looks like

- New users can sign up with email/password and verify their email
- Existing users can sign in
- The session persists across page reloads and browser restarts
- Sign-out clears the session and redirects to home
- Protected routes redirect to sign-in when accessed without auth
- The user's name and avatar appear in the navbar when signed in
- Password reset works end-to-end
- Social login takes one click

## Common mistakes to avoid

- **Storing passwords yourself.** Never. Use a managed provider.
- **Putting auth checks only in client components.** Auth must be enforced on the server (middleware, server components, route handlers). Client checks are UI hints only — they can be bypassed.
- **Forgetting Row Level Security with Supabase.** Without RLS, your auth is decorative.
- **Hardcoding redirects.** Use the provider's "redirect after sign-in" config so deep links work.
- **Skipping email verification.** Spam accounts will pile up.
- **Using `getSession()` instead of `getUser()` in Supabase server code.** `getUser()` validates the JWT; `getSession()` trusts the cookie.
- **Not adding the production domain to OAuth allowed redirects.** Sign-in with Google will fail in prod until you do.
- **Mixing auth providers mid-project.** Migrating users between providers is painful. Pick once and commit.

## Going deeper
- Authentication: https://www.codebooks.ai/authentication
- Social Login: https://www.codebooks.ai/social-login
- Security Basics: https://www.codebooks.ai/security


---

This skill is part of the **CodeBooks Vibe Coding Skills Library**.
Browse all skills, install guides, and the source chapters at
https://www.codebooks.ai/skills
