From aaf466af9f86dc40a4270077b58125e95d621836 Mon Sep 17 00:00:00 2001 From: onur sencan Date: Thu, 27 Aug 2026 18:25:49 +0300 Subject: [PATCH] Add documentation and claude skills for Clerk Core 3 migration(v6=>v7) Includes component replacements and updated examples for authentication and subscription management. Introduce new files for conversion guides and update existing lesson content to reflect changes in Clerk's API. --- .../clerk-core3-auth/SKILL.md | 100 ++ .../clerk-core3-auth/examples.md | 84 ++ .../clerk_core3_signedin_signedout.md | 121 +++ .../clerk_publishable_key_vercel.md | 40 +- week1/day3.part2_v2.md | 405 +++++++ week1/day3_v2.md | 433 ++++++++ week1/day4_v2.md | 584 ++++++++++ week1/day5_v2.md | 996 ++++++++++++++++++ week1/prior_day5_app_runner.md | 2 +- week1/prior_day5_app_runner_v2.md | 905 ++++++++++++++++ 10 files changed, 3653 insertions(+), 17 deletions(-) create mode 100644 community_contributions/clerk-core3-auth/SKILL.md create mode 100644 community_contributions/clerk-core3-auth/examples.md create mode 100644 community_contributions/clerk_core3_signedin_signedout.md create mode 100644 week1/day3.part2_v2.md create mode 100644 week1/day3_v2.md create mode 100644 week1/day4_v2.md create mode 100644 week1/day5_v2.md create mode 100644 week1/prior_day5_app_runner_v2.md diff --git a/community_contributions/clerk-core3-auth/SKILL.md b/community_contributions/clerk-core3-auth/SKILL.md new file mode 100644 index 00000000..eb84c9e1 --- /dev/null +++ b/community_contributions/clerk-core3-auth/SKILL.md @@ -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: is not available in @clerk/nextjs Core 3. +Clerk: is not available in @clerk/nextjs Core 3. +``` + +Official replacement is ``. `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** → `` + +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 ? ( + <> + Go to App + + +) : ( + ... +)} +``` + +### Protect (plan / role / permission) + +```tsx +const { isLoaded, has } = useAuth(); +const hasPremium = Boolean(isLoaded && has?.({ plan: 'premium_subscription' })); + +{hasPremium ? : } +``` + +`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 ` expr}>`, use `Boolean(isLoaded && expr)` with the same `has` from `useAuth()`. + +## Server components + +Use `` from `@clerk/nextjs`: + +| Old | New | +| --- | --- | +| `` | `` | +| `` | `` | +| `` (no props) | `` | +| `` | `` | +| `` | `` | +| `` | `` | +| ` expr}>` | ` 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) diff --git a/community_contributions/clerk-core3-auth/examples.md b/community_contributions/clerk-core3-auth/examples.md new file mode 100644 index 00000000..f933335a --- /dev/null +++ b/community_contributions/clerk-core3-auth/examples.md @@ -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'; + + + + + + + + Go to App + + +``` + +**After (client page)** + +```tsx +"use client" + +import { SignInButton, UserButton, useAuth } from '@clerk/nextjs'; + +const { isLoaded, isSignedIn } = useAuth(); +const showSignedIn = isLoaded && isSignedIn; + +{showSignedIn ? ( + <> + Go to App + + +) : ( + + + +)} +``` + +Move `afterSignOutUrl="/"` to `ClerkProvider` in `pages/_app.tsx`. + +## Product page plan gate + +**Before** + +```tsx +} +> + + +``` + +**After** + +```tsx +const { isLoaded, has } = useAuth(); +const hasPremium = Boolean(isLoaded && has?.({ plan: 'premium_subscription' })); + +{hasPremium ? : } +``` + +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'; + + + + + + + + +}> + + +``` diff --git a/community_contributions/clerk_core3_signedin_signedout.md b/community_contributions/clerk_core3_signedin_signedout.md new file mode 100644 index 00000000..df061352 --- /dev/null +++ b/community_contributions/clerk_core3_signedin_signedout.md @@ -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: is not available in @clerk/nextjs Core 3. +Clerk: 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 ``, ``, and ``. The official replacement is ``. + +`` from `@clerk/nextjs` is a **server** control. Week 1 pages are `"use client"` Pages Router files, so `` does not see the browser session. On those pages, use `useAuth()`. + +- **Client component** (`"use client"`) → `useAuth()` +- **Server component** → `` + +## SignedIn / SignedOut → useAuth() + +**Before (older lesson code)** + +```tsx +import { SignInButton, SignedIn, SignedOut, UserButton } from '@clerk/nextjs'; + + + + + + + + Go to App + + +``` + +**After** + +```tsx +"use client" + +import { SignInButton, UserButton, useAuth } from '@clerk/nextjs'; + +const { isLoaded, isSignedIn } = useAuth(); +const showSignedIn = isLoaded && isSignedIn; + +{showSignedIn ? ( + <> + Go to App + + +) : ( + + + +)} +``` + +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 + +``` + +## Protect → useAuth().has() + +Day 3 Part 2 and Day 4 used ``. That throws the same Core 3 error. + +**Before** + +```tsx +}> + + +``` + +**After** + +```tsx +const { isLoaded, has } = useAuth(); +const hasPremium = Boolean(isLoaded && has?.({ plan: 'premium_subscription' })); + +{hasPremium ? : } +``` + +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 ``: + +| Old | New | +| --- | --- | +| `` | `` | +| `` | `` | +| `` (no props) | `` | +| `` | `` | +| `` | `` | +| `` | `` | + +`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). diff --git a/community_contributions/clerk_publishable_key_vercel.md b/community_contributions/clerk_publishable_key_vercel.md index 058f2119..dcd1cf29 100644 --- a/community_contributions/clerk_publishable_key_vercel.md +++ b/community_contributions/clerk_publishable_key_vercel.md @@ -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 = @@ -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'; @@ -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 diff --git a/week1/day3.part2_v2.md b/week1/day3.part2_v2.md new file mode 100644 index 00000000..b0a8081d --- /dev/null +++ b/week1/day3.part2_v2.md @@ -0,0 +1,405 @@ +# Day 3 Part 2: Adding Subscriptions with Clerk Billing + +## Transform Your SaaS with Subscription Management + +Now let's add subscription tiers to your Business Idea Generator, turning it into a full-fledged SaaS with payment processing and subscription management built-in. + +## What You'll Build + +An enhanced version of your app that: +- Requires a paid subscription to access the idea generator +- Shows a beautiful pricing table to non-subscribers +- Handles payment processing through Clerk Billing +- Manages subscription status automatically +- Provides a user menu with billing management options + +## Prerequisites + +- Completed Day 3 Part 1 (authentication working) +- Your app deployed to Vercel + +Clerk Core 3 (`@clerk/nextjs` v7) removed ``. The product-page sample below uses `useAuth().has()` instead. Details: [clerk_core3_signedin_signedout.md](../community_contributions/clerk_core3_signedin_signedout.md). + +## Step 1: Enable Clerk Billing + +### Navigate to Clerk Dashboard + +1. Go to your [Clerk Dashboard](https://dashboard.clerk.com) +2. Select your **SaaS** application +3. Click **Configure** in the top navigation +4. Click **Billing>>Subscription Plans** in the left sidebar +5. Click **Get Started** if this is your first time + +### Enable Billing + +1. Click **Enable Billing** if prompted +2. Accept the terms if prompted +3. You'll see the Subscription Plans page + +## Step 2: Create Your Subscription Plan + +### Configure the Plan + +1. Click **Create Plan** +2. Fill in the details: + - **Name:** Premium Subscription + - **Key:** `premium_subscription` (this is important - copy it exactly) + - **Price:** $10.00 monthly (or your preferred price) + - **Description:** Unlimited AI-powered business ideas +3. Optional: Add an annual discount + - Toggle on **Annual billing** + - Set annual price (e.g., $100/year for a discount) +4. Click **Save** + +### Copy the Plan ID + +After creating the plan, you'll see a **Plan ID** in the top right of the plan card (it looks like `plan_...`). You'll need this for testing, but Clerk handles it automatically in production. + +## Step 3: Update Your Product Page + +Since we're using Pages Router with client-side components, we need to protect our product route with a subscription check. Clerk Core 3 removed ``, so this sample uses `useAuth().has()`. + +Update `pages/product.tsx`: + +```typescript +"use client" + +import { useEffect, useState } from 'react'; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; +import remarkBreaks from 'remark-breaks'; +import { fetchEventSource } from '@microsoft/fetch-event-source'; +import { PricingTable, UserButton, useAuth } from '@clerk/nextjs'; + +function IdeaGenerator() { + const { getToken } = useAuth(); + const [idea, setIdea] = useState('…loading'); + + useEffect(() => { + let buffer = ''; + (async () => { + const jwt = await getToken(); + if (!jwt) { + setIdea('Authentication required'); + return; + } + + await fetchEventSource('/api', { + headers: { Authorization: `Bearer ${jwt}` }, + onmessage(ev) { + buffer += ev.data; + setIdea(buffer); + }, + onerror(err) { + console.error('SSE error:', err); + // Don't throw - let it retry + } + }); + })(); + }, []); // Empty dependency array - run once on mount + + return ( +
+ {/* Header */} +
+

+ Business Idea Generator +

+

+ AI-powered innovation at your fingertips +

+
+ + {/* Content Card */} +
+
+ {idea === '…loading' ? ( +
+
+ Generating your business idea... +
+
+ ) : ( +
+ + {idea} + +
+ )} +
+
+
+ ); +} + +export default function Product() { + const { isLoaded, has } = useAuth(); + const hasPremium = Boolean(isLoaded && has?.({ plan: 'premium_subscription' })); + + return ( +
+ {/* User Menu in Top Right */} +
+ +
+ + {hasPremium ? ( + + ) : ( +
+
+

+ Choose Your Plan +

+

+ Unlock unlimited AI-powered business ideas +

+
+
+ +
+
+ )} +
+ ); +} +``` + +## Step 4: Update Your Landing Page + +Let's update the landing page to better reflect the subscription model. + +Update `pages/index.tsx`. Clerk Core 3 removed `` / ``, so this sample uses `useAuth()`: + +```typescript +"use client" + +import Link from 'next/link'; +import { SignInButton, UserButton, useAuth } from '@clerk/nextjs'; + +export default function Home() { + const { isLoaded, isSignedIn } = useAuth(); + const showSignedIn = isLoaded && isSignedIn; + + return ( +
+
+ {/* Navigation */} + + + {/* Hero Section */} +
+

+ Generate Your Next +
+ Big Business Idea +

+

+ Harness the power of AI to discover innovative business opportunities tailored for the AI agent economy +

+ + {/* Pricing Preview */} +
+

Premium Subscription

+

$10/month

+
    +
  • ✓ Unlimited idea generation
  • +
  • ✓ Advanced AI models
  • +
  • ✓ Priority support
  • +
+
+ + {showSignedIn ? ( + + + + ) : ( + + + + )} +
+
+
+ ); +} +``` + +## Step 5: Configure Billing Provider (Optional) + +Clerk comes with a built-in payment gateway that's ready to use immediately: + +1. In Clerk Dashboard → **Configure** → **Billing** → **Settings** (in the left sidebar) +2. By default, **Clerk payment gateway** is selected: + - "Our zero-config payment gateway. Ready to process test payments immediately." + - This works great for testing and development +3. **Optional:** You can switch to Stripe if you prefer: + - Select **Stripe** instead + - Follow Clerk's setup wizard to connect your Stripe account + +**Note:** The Clerk payment gateway is perfect for getting started - it handles test payments immediately without any additional setup. + +## Step 6: Test Your Subscription Flow + +Deploy your updated application: + +```bash +vercel --prod +``` + +### Testing the Flow + +1. Visit your production URL +2. Sign in (or create a new account) +3. Click "Go to App" or "Access Premium Features" +4. You'll see the pricing table since you don't have a subscription +5. Click **Subscribe** on the Premium plan +6. If you haven't connected a payment provider, Clerk will simulate the subscription +7. After subscribing, you'll have access to the idea generator + +### Managing Subscriptions + +Users can manage their subscriptions through the UserButton menu: +1. Click on their profile picture (UserButton) +2. Select **Manage account** +3. Navigate to **Subscriptions** +4. View or cancel their subscription + +## What's Happening? + +Your app now has: +- **Subscription Gate**: Users must have an active subscription to access the product +- **Pricing Table**: Beautiful, Clerk-managed pricing display +- **Payment Processing**: Handled entirely by Clerk (with Stripe integration if configured) +- **User Management**: Subscription status in the UserButton menu +- **Automatic Enforcement**: Clerk automatically checks subscription status + +## Architecture Overview + +1. **User visits `/product`** → Clerk checks subscription status +2. **No subscription** → Shows PricingTable component +3. **Has subscription** → Shows IdeaGenerator component +4. **Payment** → Handled by Clerk Billing (optionally with Stripe) +5. **Management** → Users manage subscriptions through Clerk's UI + +## Troubleshooting + +### "Plan not found" error +- Ensure the plan key is exactly `premium_subscription` +- Check that billing is enabled in Clerk Dashboard +- Verify the plan is active (not archived) + +### Pricing table not showing +- Clear browser cache and cookies +- Check that `@clerk/nextjs` is up to date +- Ensure billing is enabled in your Clerk application + +### Always seeing the pricing table (even after subscribing) +- Check the user's subscription status in Clerk Dashboard +- Verify the plan key matches exactly +- Try signing out and back in +- Wait until `useAuth().isLoaded` is true before treating `has()` as false + +### `Protect` / `SignedIn` is not available +- Clerk Core 3 removed those components. Use `useAuth().has()` and `useAuth().isSignedIn` as in the samples above. See [clerk_core3_signedin_signedout.md](../community_contributions/clerk_core3_signedin_signedout.md). + +### Payment not working +- This is normal if you haven't connected a payment provider +- Clerk will simulate subscriptions in test mode +- For real payments, connect Stripe in Billing Settings + +## Customization Options + +### Different Plan Tiers + +You can create multiple plans in Clerk Dashboard: +```typescript +const { isLoaded, has } = useAuth(); +const hasAccess = Boolean( + isLoaded && ( + has?.({ plan: 'basic_plan' }) || + has?.({ plan: 'premium_plan' }) || + has?.({ plan: 'enterprise_plan' }) + ) +); + +{hasAccess ? : } +``` + +### Custom Pricing Table + +Instead of Clerk's default PricingTable, you can build your own: +```typescript +const { isLoaded, has } = useAuth(); +const hasPremium = Boolean(isLoaded && has?.({ plan: 'premium_subscription' })); + +{hasPremium ? : } +``` + +### Usage Limits + +Track API usage per user in your backend: +```python +@app.get("/api") +def idea(creds: HTTPAuthorizationCredentials = Depends(clerk_guard)): + user_id = creds.decoded["sub"] + subscription_plan = creds.decoded.get("subscription", "free") + + # Apply different limits based on plan + if subscription_plan == "premium_subscription": + # Unlimited or high limit + pass + else: + # Limited access + pass +``` + +## Next Steps + +Congratulations! You've built a complete SaaS with: +- ✅ User authentication +- ✅ Subscription management +- ✅ Payment processing +- ✅ AI-powered features +- ✅ Professional UI/UX + +### Ideas for Enhancement + +1. **Multiple subscription tiers** (Basic, Pro, Enterprise) +2. **Usage tracking** and limits per tier +3. **Webhook integration** for subscription events +4. **Email notifications** for subscription changes +5. **Admin dashboard** to manage users and subscriptions +6. **Annual billing discounts** +7. **Free trial periods** + +Your Business Idea Generator is now a fully-functional SaaS product ready for real customers! \ No newline at end of file diff --git a/week1/day3_v2.md b/week1/day3_v2.md new file mode 100644 index 00000000..1cbfb4b5 --- /dev/null +++ b/week1/day3_v2.md @@ -0,0 +1,433 @@ +# Day 3: Adding User Authentication with Clerk + +## Transform Your SaaS with Professional Authentication + +Today you'll add enterprise-grade authentication to your Business Idea Generator, allowing users to sign in with Google, GitHub, and other providers. This transforms your app from a demo into a real SaaS product. + +## What You'll Build + +An authenticated version of your app that: +- Requires users to sign in before accessing the idea generator +- Supports multiple authentication providers (Google, GitHub, Email) +- Passes secure JWT tokens to your backend +- Verifies user identity on every API request +- Works seamlessly with Next.js Pages Router + +## Prerequisites + +- Completed Day 2 (working Business Idea Generator) +- Your project deployed to Vercel + +## IMPORTANT Note - added since the videos + +In some situations, if your app takes longer than 60 seconds to respond to a request, it's possible that you experience a Timeout error. You'll see in the browser's Javascript Console that you're getting a 403 error. The fix for this is in community_contributions explained in the file jwt_token_60s_fix.md. Look out for this 403 timeout after 60 seconds, and if it happens, please see the fix. Thanks! + +Clerk Core 3 (`@clerk/nextjs` v7) also removed ``, ``, and ``. The Week 1 samples now use `useAuth()`. If you copied older snippets and see those components are "not available", see [clerk_core3_signedin_signedout.md](../community_contributions/clerk_core3_signedin_signedout.md). If Sign In never appears after `vercel --prod`, see [clerk_publishable_key_vercel.md](../community_contributions/clerk_publishable_key_vercel.md). + + +## Part 1: User Authentication + +### Step 1: Create Your Clerk Account + +1. Visit [clerk.com](https://clerk.com) and click **Sign Up** +2. Create your account using Google auth (or your preferred method) +3. You'll be taken to **Create Application** (or click "Create Application" if returning) + +### Step 2: Configure Your Clerk Application + +1. **Application name:** SaaS +2. **Sign-in options:** Enable these providers: + - Email + - Google + - GitHub + - Apple (optional) +3. Click **Create Application** + +You'll see the Clerk dashboard with your API keys displayed. + +### Step 3: Install Clerk Dependencies + +Side note: The latest `@clerk/nextjs` as of Core 3 is v7. It removed ``, ``, and ``. Rendering them throws `Clerk: is not available in @clerk/nextjs Core 3`. On Week 1 Pages Router client pages, use `useAuth()` instead of those components. The code samples below are already updated. Full notes: [clerk_core3_signedin_signedout.md](../community_contributions/clerk_core3_signedin_signedout.md). + +If you want to stay on the original lesson version, pin v6: + +```bash +npm install @clerk/nextjs@6.39.0 +``` + +For handling streaming with authentication, also install: + +```bash +npm install @microsoft/fetch-event-source +``` + +### Step 4: Configure Environment Variables + +Create a `.env.local` file in your project root: + +```bash +CLERK_PUBLISHABLE_KEY=your_publishable_key_here +CLERK_SECRET_KEY=your_secret_key_here +``` + +Clerk's browser SDK auto-reads `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY`. Vercel Clerk often provisions `CLERK_PUBLISHABLE_KEY` instead. Use that name, then inline it in `next.config.ts` so it reaches the browser (see Step 5). Details: [clerk_publishable_key_vercel.md](../community_contributions/clerk_publishable_key_vercel.md). + +**Important:** Copy these values from the Clerk dashboard (they're displayed after creating your application on the configure screen, in the API Keys section). Remember to save the .env.local file after changing it. + +### Add to .gitignore + +Open `.gitignore` in Cursor and add `.env.local` on a new line. + +### Step 5: Add Clerk Provider to Your App + +With Pages Router, we need to wrap our application with the Clerk provider. Do **not** add `"use client"` to `_app.tsx`. + +Also update `next.config.ts` so `CLERK_PUBLISHABLE_KEY` is inlined into the client bundle: + +```typescript +import type { NextConfig } from "next"; + +const clerkPublishableKey = + process.env.CLERK_PUBLISHABLE_KEY || + process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY || + ''; + +const nextConfig: NextConfig = { + reactStrictMode: true, + env: { + CLERK_PUBLISHABLE_KEY: clerkPublishableKey, + }, +}; + +export default nextConfig; +``` + +Update `pages/_app.tsx`: + +```typescript +import { ClerkProvider } from '@clerk/nextjs'; +import type { AppProps } from 'next/app'; +import '../styles/globals.css'; + +export default function MyApp({ Component, pageProps }: AppProps) { + const publishableKey = process.env.CLERK_PUBLISHABLE_KEY; + + return ( + + + + ); +} +``` + +### Step 6: Create the Product Page + +Move your business idea generator to a protected route. Since we're using client-side authentication, we'll protect this route using Clerk's built-in components. + +Create `pages/product.tsx`: + +```typescript +"use client" + +import { useEffect, useState } from 'react'; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; +import remarkBreaks from 'remark-breaks'; +import { useAuth } from '@clerk/nextjs'; +import { fetchEventSource } from '@microsoft/fetch-event-source'; + +export default function Product() { + const { getToken } = useAuth(); + const [idea, setIdea] = useState('…loading'); + + useEffect(() => { + let buffer = ''; + (async () => { + const jwt = await getToken(); + if (!jwt) { + setIdea('Authentication required'); + return; + } + + await fetchEventSource('/api', { + headers: { Authorization: `Bearer ${jwt}` }, + onmessage(ev) { + buffer += ev.data; + setIdea(buffer); + }, + onerror(err) { + console.error('SSE error:', err); + // Don't throw - let it retry + } + }); + })(); + }, []); // Empty dependency array - run once on mount + + return ( +
+
+ {/* Header */} +
+

+ Business Idea Generator +

+

+ AI-powered innovation at your fingertips +

+
+ + {/* Content Card */} +
+
+ {idea === '…loading' ? ( +
+
+ Generating your business idea... +
+
+ ) : ( +
+ + {idea} + +
+ )} +
+
+
+
+ ); +} +``` + +### Step 7: Create the Landing Page + +Update `pages/index.tsx` to be your new landing page with sign-in. Clerk Core 3 removed `` / ``, so this sample uses `useAuth()`: + +```typescript +"use client" + +import Link from 'next/link'; +import { SignInButton, UserButton, useAuth } from '@clerk/nextjs'; + +export default function Home() { + const { isLoaded, isSignedIn } = useAuth(); + const showSignedIn = isLoaded && isSignedIn; + + return ( +
+
+ {/* Navigation */} + + + {/* Hero Section */} +
+

+ Generate Your Next +
+ Big Business Idea +

+

+ Harness the power of AI to discover innovative business opportunities tailored for the AI agent economy +

+ + {showSignedIn ? ( + + + + ) : ( + + + + )} +
+
+
+ ); +} +``` + +### Step 8: Configure Backend Authentication + +First, get your JWKS URL from Clerk: +1. Go to your Clerk Dashboard +2. Click **Configure** (top nav) +3. Click **API Keys** (side nav) +4. Find **JWKS URL** and copy it + +**What is JWKS?** The JWKS (JSON Web Key Set) URL is a public endpoint that contains Clerk's public keys. When a user signs in, Clerk creates a JWT (JSON Web Token) - a digitally signed token that proves the user's identity. Your Python backend uses the JWKS URL to fetch Clerk's public keys and verify that incoming JWT tokens are genuine and haven't been tampered with. This allows secure authentication without your backend needing to contact Clerk for every request - it can verify tokens independently using cryptographic signatures. + +Add to `.env.local` and save: +```bash +CLERK_JWKS_URL=your_jwks_url_here +``` + +### Step 9: Update Backend Dependencies + +Add the Clerk authentication library to `requirements.txt`: + +``` +fastapi +uvicorn +openai +fastapi-clerk-auth +``` + +### Step 10: Update the API with Authentication + +Replace `api/index.py` with: + +```python +import os +from fastapi import FastAPI, Depends # type: ignore +from fastapi.responses import StreamingResponse # type: ignore +from fastapi_clerk_auth import ClerkConfig, ClerkHTTPBearer, HTTPAuthorizationCredentials # type: ignore +from openai import OpenAI # type: ignore + +app = FastAPI() + +clerk_config = ClerkConfig(jwks_url=os.getenv("CLERK_JWKS_URL")) +clerk_guard = ClerkHTTPBearer(clerk_config) + +@app.get("/api") +def idea(creds: HTTPAuthorizationCredentials = Depends(clerk_guard)): + user_id = creds.decoded["sub"] # User ID from JWT - available for future use + # We now know which user is making the request! + # You could use user_id to: + # - Track usage per user + # - Store generated ideas in a database + # - Apply user-specific limits or customization + + client = OpenAI() + prompt = [{"role": "user", "content": "Reply with a new business idea for AI Agents, formatted with headings, sub-headings and bullet points"}] + stream = client.chat.completions.create(model="gpt-5-nano", messages=prompt, stream=True) + + def event_stream(): + for chunk in stream: + text = chunk.choices[0].delta.content + if text: + lines = text.split("\n") + for line in lines[:-1]: + yield f"data: {line}\n\n" + yield "data: \n" + yield f"data: {lines[-1]}\n\n" + + return StreamingResponse(event_stream(), media_type="text/event-stream") +``` + +### Step 11: Add Environment Variables to Vercel + +Add your Clerk keys to Vercel: + +```bash +vercel env add CLERK_PUBLISHABLE_KEY +``` +Paste your publishable key and select all environments. If Vercel lets you add `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` instead, that also works — the `next.config.ts` snippet in Step 5 accepts either name. + +```bash +vercel env add CLERK_SECRET_KEY +``` +Paste your secret key and select all environments except for development. + +```bash +vercel env add CLERK_JWKS_URL +``` +Paste your JWKS URL and select all environments except for development. + +### Step 12: (This step intentionally skipped - we will test after deployment) + +### Step 13: Deploy to Production + +Deploy your authenticated app: + +```bash +vercel --prod +``` + +Visit your production URL and test the complete authentication flow! + +NOTE - if you hit a problem with jwt token expiration, please see this [fix contributed by Artur P](../community_contributions/jwt_token_60s_fix.md). If Sign In never appears after `vercel --prod`, see [clerk_publishable_key_vercel.md](../community_contributions/clerk_publishable_key_vercel.md). If you get `SignedOut` / `Protect` is not available, see [clerk_core3_signedin_signedout.md](../community_contributions/clerk_core3_signedin_signedout.md). + +## What's Happening? + +Your app now has: +- **Secure authentication**: Users must sign in to access your product +- **Client-side route protection**: Unauthenticated users are redirected from protected pages +- **JWT verification**: Every API request is verified using cryptographic signatures +- **User identification**: The backend knows which user is making each request +- **Professional UX**: Modal sign-in, user profile management, and smooth redirects +- **Multiple providers**: Users can choose their preferred sign-in method + +## Security Architecture + +Since we're using client-side Next.js with a separate Python backend: + +1. **Frontend (Browser)**: User signs in with Clerk → receives session token +2. **Client-Side Protection**: Protected routes check authentication status and redirect if needed +3. **API Request**: Browser sends JWT token directly to Python backend with each request +4. **Backend Verification**: FastAPI verifies the JWT using Clerk's public keys (JWKS) +5. **User Context**: Backend can access user ID and metadata from verified token + +This architecture keeps your Next.js deployment simple (static/client-side only) while maintaining secure API authentication. + +## Troubleshooting + +### "Unauthorized" errors +- Check that all three environment variables are set correctly in Vercel +- Ensure the JWKS URL is copied correctly from Clerk +- Verify you're signed in before accessing `/product` + +### Sign-in modal not appearing +- Check that `CLERK_PUBLISHABLE_KEY` (or `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY`) starts with `pk_` +- Confirm `next.config.ts` inlines the key and `ClerkProvider` receives `publishableKey` +- Ensure you've wrapped your app with `ClerkProvider` and `_app.tsx` does **not** have `"use client"` +- Clear browser cache and cookies + +### API not authenticating +- Verify `CLERK_JWKS_URL` is set in your environment +- Check that `fastapi-clerk-auth` is in requirements.txt +- Ensure the JWT token is being sent in the Authorization header + +### Local development issues +- Make sure `.env.local` has all three Clerk variables +- Restart your dev server after adding environment variables +- Try clearing Next.js cache: `rm -rf .next` + +## Next Steps + +Congratulations! You've added professional authentication to your SaaS. In Part 2, we'll add: +- Subscription tiers with Stripe +- Usage limits based on subscription level +- Payment processing +- Customer portal for managing subscriptions + +Your app is now a real SaaS product with secure user authentication! \ No newline at end of file diff --git a/week1/day4_v2.md b/week1/day4_v2.md new file mode 100644 index 00000000..21122f1e --- /dev/null +++ b/week1/day4_v2.md @@ -0,0 +1,584 @@ +# Day 4: Healthcare Consultation Assistant + +## Build a Professional Healthcare Application + +Today, you'll transform your SaaS into a healthcare consultation assistant that helps doctors generate patient summaries, action items, and patient-friendly emails from their visit notes. + +## What You'll Build + +A healthcare application that: +- Takes doctor's consultation notes as input +- Generates professional summaries for medical records +- Creates actionable next steps for the doctor +- Drafts patient-friendly email communications +- Uses structured forms with date pickers +- Streams AI-generated content in real-time + +## Prerequisites + +- Completed Day 3 (authentication and subscriptions working) +- Your app deployed to Vercel + +Clerk Core 3 removed `` and `` / ``. The samples below use `useAuth()`. Do not mount `ConsultationForm` until the plan check is true. Details: [clerk_core3_signedin_signedout.md](../community_contributions/clerk_core3_signedin_signedout.md). + +## Step 1: Install Additional Dependencies + +We need a date picker for the consultation form: + +```bash +npm install react-datepicker +npm install --save-dev @types/react-datepicker +``` + +## Step 2: Update the Backend API + +Replace `api/index.py` with a new endpoint that handles consultation data: + +```python +import os +from fastapi import FastAPI, Depends # type: ignore +from fastapi.responses import StreamingResponse # type: ignore +from pydantic import BaseModel # type: ignore +from fastapi_clerk_auth import ClerkConfig, ClerkHTTPBearer, HTTPAuthorizationCredentials # type: ignore +from openai import OpenAI # type: ignore + +app = FastAPI() +clerk_config = ClerkConfig(jwks_url=os.getenv("CLERK_JWKS_URL")) +clerk_guard = ClerkHTTPBearer(clerk_config) + + +class Visit(BaseModel): + patient_name: str + date_of_visit: str + notes: str + + +system_prompt = """ +You are provided with notes written by a doctor from a patient's visit. +Your job is to summarize the visit for the doctor and provide an email. +Reply with exactly three sections with the headings: +### Summary of visit for the doctor's records +### Next steps for the doctor +### Draft of email to patient in patient-friendly language +""" + + +def user_prompt_for(visit: Visit) -> str: + return f"""Create the summary, next steps and draft email for: +Patient Name: {visit.patient_name} +Date of Visit: {visit.date_of_visit} +Notes: +{visit.notes}""" + + +@app.post("/api") +def consultation_summary( + visit: Visit, + creds: HTTPAuthorizationCredentials = Depends(clerk_guard), +): + user_id = creds.decoded["sub"] # Available for tracking/auditing + client = OpenAI() + + user_prompt = user_prompt_for(visit) + + prompt = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt}, + ] + + stream = client.chat.completions.create( + model="gpt-5-nano", + messages=prompt, + stream=True, + ) + + def event_stream(): + for chunk in stream: + text = chunk.choices[0].delta.content + if text: + lines = text.split("\n") + for line in lines[:-1]: + yield f"data: {line}\n\n" + yield "data: \n" + yield f"data: {lines[-1]}\n\n" + + return StreamingResponse(event_stream(), media_type="text/event-stream") +``` + +Note the key changes: +- Changed from `@app.get("/api")` to `@app.post("/api")` to accept form data +- Added a `Visit` model to validate incoming data +- Structured prompts for healthcare-specific output + +## Step 3: Update Application Configuration + +First, import the date picker styles in `pages/_app.tsx`: + +```typescript +import { ClerkProvider } from '@clerk/nextjs'; +import type { AppProps } from 'next/app'; +import 'react-datepicker/dist/react-datepicker.css'; +import '../styles/globals.css'; + +export default function MyApp({ Component, pageProps }: AppProps) { + return ( + + + + ); +} +``` + +Now update `pages/_document.tsx` to reflect the healthcare focus: + +```typescript +import { Html, Head, Main, NextScript } from 'next/document'; + +export default function Document() { + return ( + + + Healthcare Consultation Assistant + + + +
+ + + + ); +} +``` + +## Step 4: Create the Consultation Form + +Replace `pages/product.tsx` with the new healthcare interface: + +```typescript +"use client" + +import { useState, FormEvent } from 'react'; +import { PricingTable, UserButton, useAuth } from '@clerk/nextjs'; +import DatePicker from 'react-datepicker'; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; +import remarkBreaks from 'remark-breaks'; +import { fetchEventSource } from '@microsoft/fetch-event-source'; + +function ConsultationForm() { + const { getToken } = useAuth(); + + // Form state + const [patientName, setPatientName] = useState(''); + const [visitDate, setVisitDate] = useState(new Date()); + const [notes, setNotes] = useState(''); + + // Streaming state + const [output, setOutput] = useState(''); + const [loading, setLoading] = useState(false); + + async function handleSubmit(e: FormEvent) { + e.preventDefault(); + setOutput(''); + setLoading(true); + + const jwt = await getToken(); + if (!jwt) { + setOutput('Authentication required'); + setLoading(false); + return; + } + + const controller = new AbortController(); + let buffer = ''; + + await fetchEventSource('/api', { + signal: controller.signal, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${jwt}`, + }, + body: JSON.stringify({ + patient_name: patientName, + date_of_visit: visitDate?.toISOString().slice(0, 10), + notes, + }), + onmessage(ev) { + buffer += ev.data; + setOutput(buffer); + }, + onclose() { + setLoading(false); + }, + onerror(err) { + console.error('SSE error:', err); + controller.abort(); + setLoading(false); + }, + }); + } + + return ( +
+

+ Consultation Notes +

+ +
+
+ + setPatientName(e.target.value)} + className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:text-white" + placeholder="Enter patient's full name" + /> +
+ +
+ + setVisitDate(d)} + dateFormat="yyyy-MM-dd" + placeholderText="Select date" + required + className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:text-white" + /> +
+ +
+ +