Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions community_contributions/clerk-core3-auth/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
---
name: clerk-core3-auth
description: Converts Clerk SignedIn, SignedOut, and Protect to Core 3 APIs (useAuth or Show). Use when @clerk/nextjs throws signedout-is-not-available, signedin-is-not-available, or protect-is-not-available, when lesson code uses SignedIn/SignedOut/Protect, when Show is used on a client page, or when migrating Clerk v6 to v7 / Core 3.
---

# Clerk Core 3 auth conversion

Copy this folder into your own SaaS repo as `.cursor/skills/clerk-core3-auth/` if you already built Week 1 on `@clerk/nextjs@6.39.0` and want Cursor to convert the old components.

`SignedIn`, `SignedOut`, and `Protect` were removed in Clerk Core 3 (`@clerk/nextjs` v7). They still export, but rendering them throws:

```text
Clerk: <SignedOut> is not available in @clerk/nextjs Core 3.
Clerk: <Protect> is not available in @clerk/nextjs Core 3.
```

Official replacement is `<Show>`. `Show` from `@clerk/nextjs` is a **server** control. On any `"use client"` file it does not see the browser session. Decide by component type, not router:

- **Client component** (`"use client"`) → `useAuth()`
- **Server component** → `<Show>`

Week 1 pages are client components, so default to `useAuth()`.

## Workflow

1. Search the repo (exclude `node_modules` and lesson markdown unless the user asked to edit lessons):

```bash
rg -n --glob '!node_modules' --glob '!week1/**' 'SignedIn|SignedOut|Protect' .
```

2. Convert every match. Keep the user's copy, layout, and extra props (`showName`, button labels).
3. Grep again. No TS/TSX file should import `SignedIn`, `SignedOut`, or `Protect`.
4. Do not render `null` while `!isLoaded`. Default to the signed-out or locked UI until Clerk confirms state.

## Client components (default here)

Use `useAuth()`.

### SignedIn / SignedOut

```tsx
"use client"

import { SignInButton, UserButton, useAuth } from '@clerk/nextjs';

const { isLoaded, isSignedIn } = useAuth();
const showSignedIn = isLoaded && isSignedIn;

{showSignedIn ? (
<>
<Link href="/product">Go to App</Link>
<UserButton showName={true} />
</>
) : (
<SignInButton mode="modal">...</SignInButton>
)}
```

### Protect (plan / role / permission)

```tsx
const { isLoaded, has } = useAuth();
const hasPremium = Boolean(isLoaded && has?.({ plan: 'premium_subscription' }));

{hasPremium ? <IdeaGenerator /> : <PricingTable />}
```

`has()` also accepts `{ role: '...' }`, `{ permission: '...' }`, `{ feature: '...' }`.

Do not mount data-fetching children (for example `IdeaGenerator` SSE) until the plan check is true, or the request fires for non-subscribers.

For `<Protect condition={(has) => expr}>`, use `Boolean(isLoaded && expr)` with the same `has` from `useAuth()`.

## Server components

Use `<Show>` from `@clerk/nextjs`:

| Old | New |
| --- | --- |
| `<SignedIn>` | `<Show when="signed-in">` |
| `<SignedOut>` | `<Show when="signed-out">` |
| `<Protect>` (no props) | `<Show when="signed-in">` |
| `<Protect plan="x">` | `<Show when={{ plan: "x" }}>` |
| `<Protect role="x">` | `<Show when={{ role: "x" }}>` |
| `<Protect permission="x">` | `<Show when={{ permission: "x" }}>` |
| `<Protect condition={(has) => expr}>` | `<Show when={(has) => expr}>` |

`Show` accepts `fallback={...}` for the failed-condition UI.

## Related Core 3 fixes (same conversion)

- `UserButton` no longer takes `afterSignOutUrl`. Set it on `ClerkProvider`.
- Keep `pages/_app.tsx` **without** `"use client"`. That directive can make Clerk pick the App Router provider, and `clerk-js` never loads.
- Next.js 16: `proxy.ts` with `clerkMiddleware()`, not `middleware.ts`.

## Additional resources

- Before/after: [examples.md](examples.md)
- Course note: [../clerk_core3_signedin_signedout.md](../clerk_core3_signedin_signedout.md)
84 changes: 84 additions & 0 deletions community_contributions/clerk-core3-auth/examples.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Clerk Core 3 conversion examples

## Landing page nav + hero

**Before (lesson / Clerk v6)**

```tsx
import { SignInButton, SignedIn, SignedOut, UserButton } from '@clerk/nextjs';

<SignedOut>
<SignInButton mode="modal">
<button>Sign In</button>
</SignInButton>
</SignedOut>
<SignedIn>
<Link href="/product">Go to App</Link>
<UserButton afterSignOutUrl="/" />
</SignedIn>
```

**After (client page)**

```tsx
"use client"

import { SignInButton, UserButton, useAuth } from '@clerk/nextjs';

const { isLoaded, isSignedIn } = useAuth();
const showSignedIn = isLoaded && isSignedIn;

{showSignedIn ? (
<>
<Link href="/product">Go to App</Link>
<UserButton showName={true} />
</>
) : (
<SignInButton mode="modal">
<button>Sign In</button>
</SignInButton>
)}
```

Move `afterSignOutUrl="/"` to `ClerkProvider` in `pages/_app.tsx`.

## Product page plan gate

**Before**

```tsx
<Protect
plan="premium_subscription"
fallback={<PricingTable />}
>
<IdeaGenerator />
</Protect>
```

**After**

```tsx
const { isLoaded, has } = useAuth();
const hasPremium = Boolean(isLoaded && has?.({ plan: 'premium_subscription' }));

{hasPremium ? <IdeaGenerator /> : <PricingTable />}
```

Do not mount `IdeaGenerator` until `hasPremium` is true, or the SSE request fires for non-subscribers.

## Server components only

```tsx
import { Show, SignInButton, UserButton } from '@clerk/nextjs';

<Show when="signed-out">
<SignInButton mode="modal" />
</Show>
<Show when="signed-in">
<UserButton />
</Show>

<Show when={{ plan: 'premium_subscription' }} fallback={<PricingTable />}>
<IdeaGenerator />
</Show>
```
121 changes: 121 additions & 0 deletions community_contributions/clerk_core3_signedin_signedout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Clerk Core 3: replace SignedIn, SignedOut, and Protect

**Week 1 Day 3 / Day 3 Part 2 / Day 4** | `@clerk/nextjs` v7 (Core 3) on Pages Router

**By Onur Sencan**

If you installed the latest Clerk (`@clerk/nextjs` v7) instead of pinning `6.39.0`, the landing page and product page can crash with:

```text
Clerk: <SignedOut> is not available in @clerk/nextjs Core 3.
Clerk: <Protect> is not available in @clerk/nextjs Core 3.
```

Those components still export so the build does not fail with "undefined is not a component". Rendering them throws that error instead.

Updated lesson copies are in `week1/day3_v2.md`, `week1/day3.part2_v2.md`, and `week1/day4_v2.md`. The original `day3.md` / `day4.md` files still match the current videos.

If you already built Week 1 on v6 and want Cursor to convert your app, copy [clerk-core3-auth](clerk-core3-auth/) into your SaaS repo as `.cursor/skills/clerk-core3-auth/`.

## What changed

Clerk Core 3 removed `<SignedIn>`, `<SignedOut>`, and `<Protect>`. The official replacement is `<Show>`.

`<Show>` from `@clerk/nextjs` is a **server** control. Week 1 pages are `"use client"` Pages Router files, so `<Show>` does not see the browser session. On those pages, use `useAuth()`.

- **Client component** (`"use client"`) → `useAuth()`
- **Server component** → `<Show>`

## SignedIn / SignedOut → useAuth()

**Before (older lesson code)**

```tsx
import { SignInButton, SignedIn, SignedOut, UserButton } from '@clerk/nextjs';

<SignedOut>
<SignInButton mode="modal">
<button>Sign In</button>
</SignInButton>
</SignedOut>
<SignedIn>
<Link href="/product">Go to App</Link>
<UserButton afterSignOutUrl="/" />
</SignedIn>
```

**After**

```tsx
"use client"

import { SignInButton, UserButton, useAuth } from '@clerk/nextjs';

const { isLoaded, isSignedIn } = useAuth();
const showSignedIn = isLoaded && isSignedIn;

{showSignedIn ? (
<>
<Link href="/product">Go to App</Link>
<UserButton />
</>
) : (
<SignInButton mode="modal">
<button>Sign In</button>
</SignInButton>
)}
```

Do **not** render `null` while `!isLoaded`. If Clerk is slow or the publishable key is missing, every CTA disappears. Default to the signed-out UI until `isLoaded && isSignedIn`.

`UserButton` no longer accepts `afterSignOutUrl`. Set it on `ClerkProvider` in `pages/_app.tsx`:

```tsx
<ClerkProvider {...pageProps} publishableKey={publishableKey} afterSignOutUrl="/">
```

## Protect → useAuth().has()

Day 3 Part 2 and Day 4 used `<Protect plan="premium_subscription">`. That throws the same Core 3 error.

**Before**

```tsx
<Protect plan="premium_subscription" fallback={<PricingTable />}>
<IdeaGenerator />
</Protect>
```

**After**

```tsx
const { isLoaded, has } = useAuth();
const hasPremium = Boolean(isLoaded && has?.({ plan: 'premium_subscription' }));

{hasPremium ? <IdeaGenerator /> : <PricingTable />}
```

Do not mount `IdeaGenerator` (or the Day 4 `ConsultationForm`) until `hasPremium` is true, or the SSE request fires for people without a plan.

`has()` also accepts `{ role: '...' }`, `{ permission: '...' }`, `{ feature: '...' }`.

## If you are on a server component

Use `<Show>`:

| Old | New |
| --- | --- |
| `<SignedIn>` | `<Show when="signed-in">` |
| `<SignedOut>` | `<Show when="signed-out">` |
| `<Protect>` (no props) | `<Show when="signed-in">` |
| `<Protect plan="x">` | `<Show when={{ plan: "x" }}>` |
| `<Protect role="x">` | `<Show when={{ role: "x" }}>` |
| `<Protect permission="x">` | `<Show when={{ permission: "x" }}>` |

`Show` accepts `fallback={...}` for the failed-condition UI.

## Related gotchas

- Keep `pages/_app.tsx` **without** `"use client"`. That directive can make Clerk pick the App Router provider, and clerk-js never loads.
- Next.js 16 uses `proxy.ts` with `clerkMiddleware()`, not `middleware.ts`.
- If Sign In never appears after `vercel --prod`, see [clerk_publishable_key_vercel.md](clerk_publishable_key_vercel.md).
40 changes: 24 additions & 16 deletions community_contributions/clerk_publishable_key_vercel.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,27 @@

**Week 1 Day 3** | Clerk on Next.js Pages Router + `vercel --prod`

**By Onur Sencan**

## The problem

Day 3 tells you to add:
Day 3 originally told you to add:

```bash
vercel env add NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY
```

Vercel Clerk often provisions CLERK_PUBLISHABLE_KEY instead. The NEXT_PUBLIC_ name can be blocked or awkward to store as a production env var.
Vercel Clerk often provisions `CLERK_PUBLISHABLE_KEY` instead. The `NEXT_PUBLIC_` name can be blocked or awkward to store as a production env var.

Clerk's browser SDK only auto-reads NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY. If that name is missing, clerk-js never loads. useAuth().isLoaded stays false, and the homepage CTAs never appear after vercel --prod.
Clerk's browser SDK only auto-reads `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY`. If that name is missing, clerk-js never loads. `useAuth().isLoaded` stays false, and the homepage CTAs never appear after `vercel --prod`.

## The fix
Keep the Vercel env var as CLERK_PUBLISHABLE_KEY. Then expose it to the browser at build time. But it needs change in `next.config.ts` as:

```typescript
Keep the Vercel env var as `CLERK_PUBLISHABLE_KEY`. Then expose it to the browser at **build time**.

### 1. `next.config.ts`

```ts
import type { NextConfig } from "next";

const clerkPublishableKey =
Expand All @@ -35,11 +40,11 @@ const nextConfig: NextConfig = {
export default nextConfig;
```

env in next.config.ts inlines the value into the client bundle. Next.js will not do that for a non-NEXT_PUBLIC_ var on its own. so change the `_app.tsx`:
`env` in `next.config.ts` inlines the value into the client bundle. Next.js will not do that for a non-`NEXT_PUBLIC_` var on its own.

2. pages/_app.tsx
### 2. `pages/_app.tsx`

```typescript
```tsx
import { ClerkProvider } from '@clerk/nextjs';
import type { AppProps } from 'next/app';
import '../styles/globals.css';
Expand All @@ -55,25 +60,28 @@ export default function MyApp({ Component, pageProps }: AppProps) {
}
```

Note: DO NOT add "use client" to _app.tsx. That can make Clerk pick the App Router provider on Pages Router.
Do **not** add `"use client"` to `_app.tsx`. That can make Clerk pick the App Router provider on Pages Router.

### 3. Vercel env vars

3. Vercel env vars
```bash
vercel env add CLERK_PUBLISHABLE_KEY
vercel env add CLERK_SECRET_KEY
vercel env add CLERK_JWKS_URL
```

If Vercel lets you add `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` instead, that also works — the `next.config.ts` snippet accepts either name.

Then rebuild:

```bash
```bash
vercel --prod
```

NEXT_PUBLIC_ values are baked in at build time. Changing the env var without a new deploy will not fix production.
`NEXT_PUBLIC_` values are baked in at build time. Changing the env var without a new deploy will not fix production.

How to tell it worked
Production HTML still has Sign In / Get Started Free
Browser console shows Clerk loaded with development keys (for pk_test_)
After sign-in, the product CTA appears
## How to tell it worked

- Production HTML still has **Sign In** / **Get Started Free**
- Browser console shows Clerk loaded with development keys (for `pk_test_`)
- After sign-in, the product CTA appears
Loading