Skip to content

feat: Migrate authentication from Clerk to WorkOS AuthKit#149

Merged
Jackson57279 merged 2 commits intomasterfrom
feature/workos-migration
Nov 28, 2025
Merged

feat: Migrate authentication from Clerk to WorkOS AuthKit#149
Jackson57279 merged 2 commits intomasterfrom
feature/workos-migration

Conversation

@Jackson57279
Copy link
Owner

@Jackson57279 Jackson57279 commented Nov 28, 2025

  • Replace @clerk/nextjs with @workos-inc/authkit-nextjs
  • Update middleware to use WorkOS authkitMiddleware
  • Create OAuth callback and sign-out routes
  • Update Convex provider to integrate with WorkOS sessions
  • Update auth server helpers to use WorkOS functions
  • Update user control component with WorkOS hooks
  • Update sign-in/sign-up pages to redirect to WorkOS
  • Update navbar with WorkOS authentication buttons
  • Configure Convex auth.config.ts for WorkOS JWKS
  • Create WorkOS webhook handler for user events
  • Delete Clerk-specific files (clerk-config.ts, webhooks/clerk)
  • Update documentation (CLAUDE.md, AGENTS.md, README.md)
  • Add .env.example with WorkOS environment variables
  • Create comprehensive WorkOS migration guide

This migration provides better pricing (free up to 1M users) and
enterprise-ready authentication with seamless Convex integration.

Summary by CodeRabbit

  • New Features

    • Switched to WorkOS AuthKit for authentication; sign-in and sign-up now redirect to hosted WorkOS flows and sign-out redirects to root.
    • Added WorkOS webhook endpoint to handle user created/updated/deleted events.
  • Documentation

    • Added a WorkOS migration guide and updated README and docs with new environment variables and setup steps.
  • Chores

    • Replaced Clerk dependency with WorkOS AuthKit and updated auth-related configuration and env vars.

✏️ Tip: You can customize this high-level summary in your review settings.

- Replace @clerk/nextjs with @workos-inc/authkit-nextjs
- Update middleware to use WorkOS authkitMiddleware
- Create OAuth callback and sign-out routes
- Update Convex provider to integrate with WorkOS sessions
- Update auth server helpers to use WorkOS functions
- Update user control component with WorkOS hooks
- Update sign-in/sign-up pages to redirect to WorkOS
- Update navbar with WorkOS authentication buttons
- Configure Convex auth.config.ts for WorkOS JWKS
- Create WorkOS webhook handler for user events
- Delete Clerk-specific files (clerk-config.ts, webhooks/clerk)
- Update documentation (CLAUDE.md, AGENTS.md, README.md)
- Add .env.example with WorkOS environment variables
- Create comprehensive WorkOS migration guide

This migration provides better pricing (free up to 1M users) and
enterprise-ready authentication with seamless Convex integration.
@codecapyai
Copy link

codecapyai bot commented Nov 28, 2025

CodeCapy Review ₍ᐢ•(ܫ)•ᐢ₎

Codebase Summary

ZapDev is an AI-powered development platform that lets users create web applications using AI agents and real-time sandboxes. The system uses Next.js for the frontend, Convex as a real-time database backend, and integrates various authentication and API services to provide features like sign-in/sign-up, live code previews, file management, and more.

PR Changes

This pull request migrates authentication from Clerk to WorkOS AuthKit. Changes include replacing Clerk components with WorkOS equivalents in sign-in, sign-up, OAuth callback, sign-out routes, and updating Convex provider integration. Documentation and configuration files (AGENTS.md, CLAUDE.md, README.md, .env.example) have been updated accordingly. Clerk-specific files and code (including middleware, user-control, and configuration files) have been removed or refactored to work with WorkOS.

Setup Instructions

  1. Install pnpm globally (if not already installed): sudo npm install -g pnpm
  2. Clone the repository and cd into it.
  3. Install dependencies by running: pnpm install
  4. Set up your environment variables by copying .env.example to .env and populating WorkOS and other required keys.
  5. Start the development server using: pnpm dev
  6. Open your browser and navigate to http://localhost:3000 to begin testing.

Generated Test Cases

1: WorkOS Sign In Redirection ❗️❗️❗️

Description: Verifies that when a user navigates to the sign in page, they are redirected to the WorkOS hosted authentication UI.

Prerequisites:

  • User is not signed in.
  • Application running locally with all WorkOS environment variables set.

Steps:

  1. Open a browser and navigate to http://localhost:3000/sign-in.
  2. Observe that the page immediately redirects (via next/navigation redirect) to the WorkOS sign in URL provided by getSignInUrl.
  3. Check the browser’s address bar to ensure the URL belongs to the WorkOS domain or contains WorkOS sign in parameters.

Expected Result: The browser should be redirected to the WorkOS authentication interface. The URL should change to that provided by WorkOS AuthKit sign in endpoint.

2: WorkOS Sign Up Redirection ❗️❗️❗️

Description: Ensures that navigation to the sign-up page redirects the user to the WorkOS sign up interface.

Prerequisites:

  • User is not signed in.
  • Application running with proper WorkOS config.

Steps:

  1. Open a browser and navigate to http://localhost:3000/sign-up.
  2. Observe the page redirection which should immediately send you to the WorkOS sign up URL obtained from getSignUpUrl.
  3. Verify the URL in the browser changes accordingly.

Expected Result: The sign-up page should not render local UI; instead the browser is redirected to the WorkOS sign up endpoint.

3: OAuth Callback Handling ❗️❗️❗️

Description: Tests that the OAuth callback route correctly processes the WorkOS authentication response and establishes a user session.

Prerequisites:

  • A simulated or real WorkOS authentication process that redirects back to http://localhost:3000/callback.
  • WorkOS environment variables correctly set.

Steps:

  1. Simulate a successful authentication by manually navigating to http://localhost:3000/callback (or use a tool to simulate a callback request).
  2. Ensure that the callback route (implemented via handleAuth) processes the request without error.
  3. After processing, verify that the application displays elements intended for authenticated users (e.g., user control component in the navbar).

Expected Result: The OAuth callback route should process the incoming authentication data, establish a valid session, and the app should render authenticated UI elements.

4: User Control Correct Data & Sign Out ❗️❗️❗️

Description: Verifies that the user control component displays the proper WorkOS user details and that sign-out functionality works.

Prerequisites:

  • User must be signed in via WorkOS.
  • Application running locally.

Steps:

  1. Navigate to the home page (http://localhost:3000) where the navbar displays the user control element.
  2. Observe that the user control shows the user’s full name (composed of firstName and lastName), email, and a profile picture if available.
  3. Click the dropdown on the user control and then click the Sign Out option.
  4. Verify that after sign-out, the user is redirected to the landing page and the authenticated UI is no longer shown.

Expected Result: The user control displays correct user information from WorkOS. When sign-out is clicked, the session is ended and the UI updates to a signed-out state.

5: Protected Route Middleware Redirection ❗️❗️

Description: Checks that middleware prevents unauthenticated access to protected routes and redirects the user to sign in.

Prerequisites:

  • User is not signed in.
  • Application running with WorkOS auth middleware enabled.

Steps:

  1. Attempt to navigate to a protected route (for example, a dashboard or any non-public page, e.g., http://localhost:3000/dashboard).
  2. The middleware should intercept the request.
  3. Observe that the user is redirected to the sign-in page (which in turn redirects to WorkOS sign in UI).

Expected Result: Access to the protected route is blocked and user is redirected to sign-in. The redirection should follow the WorkOS authentication flow.

6: WorkOS Webhook Handler Response ❗️❗️

Description: Ensures that the webhook endpoint for WorkOS events processes incoming requests correctly.

Prerequisites:

  • The application is running locally.
  • A tool like Postman or curl is available to send HTTP POST requests.

Steps:

  1. Craft a sample POST request to http://localhost:3000/api/webhooks/workos with headers including a 'workos-signature' (a dummy value is acceptable for testing) and a JSON payload representing a WorkOS event (e.g., event type 'user.created' with appropriate data).
  2. Send the request.
  3. Observe the response status code and any console logs in the backend server output.

Expected Result: The webhook endpoint should return an HTTP 200 response indicating successful processing. The server logs should indicate handling of the event (e.g., 'User created: ...' printed to console).

Raw Changes Analyzed
File: AGENTS.md
Changes:
@@ -39,7 +39,7 @@ e2b template build --name your-template-name --cmd "/compile_page.sh"
 ### Tech Stack
 - **Frontend**: Next.js 15 (App Router), React 19, TypeScript, Tailwind CSS v4, Shadcn/ui
 - **Backend**: Convex (real-time database), tRPC (type-safe APIs)
-- **Auth**: Clerk with JWT authentication
+- **Auth**: WorkOS AuthKit with JWT authentication
 - **AI**: Vercel AI Gateway (Claude via Anthropic), Inngest Agent Kit
 - **Code Execution**: E2B Code Interpreter (isolated sandboxes)
 - **Background Jobs**: Inngest
@@ -123,9 +123,10 @@ Required for development:
 - `AI_GATEWAY_API_KEY`: Vercel AI Gateway key
 - `AI_GATEWAY_BASE_URL`: https://ai-gateway.vercel.sh/v1/
 - `E2B_API_KEY`: E2B sandbox API key
-- `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY`: Clerk publishable key
-- `CLERK_SECRET_KEY`: Clerk secret key
-- `CLERK_JWT_ISSUER_DOMAIN`: Clerk JWT issuer domain (from dashboard)
+- `WORKOS_API_KEY`: WorkOS API key (from dashboard)
+- `WORKOS_CLIENT_ID`: WorkOS client ID (from dashboard)
+- `WORKOS_REDIRECT_URI`: Auth callback URL (e.g., http://localhost:3000/callback)
+- `WORKOS_WEBHOOK_SECRET`: WorkOS webhook secret (from dashboard)
 - `INNGEST_EVENT_KEY`: Inngest event key
 - `INNGEST_SIGNING_KEY`: Inngest signing key
 

File: CLAUDE.md
Changes:
@@ -10,7 +10,7 @@ ZapDev is an AI-powered development platform that enables users to create web ap
 
 **Frontend**: Next.js 15 (Turbopack), React 19, TypeScript 5.9, Tailwind CSS v4, Shadcn/ui, React Query
 **Backend**: Convex (real-time database), tRPC (type-safe APIs)
-**Authentication**: Clerk (user auth & JWT)
+**Authentication**: WorkOS AuthKit (user auth & JWT)
 **AI & Execution**: Vercel AI Gateway, Inngest 3.44 (job orchestration), E2B Code Interpreter (sandboxes)
 **Monitoring**: Sentry, OpenTelemetry
 
@@ -176,6 +176,8 @@ Subscriptions enable real-time UI updates when data changes.
 
 **Query Client**: React Query configured in `src/trpc/query-client.ts` for caching, refetching, and optimistic updates.
 
+**Authentication Flow**: WorkOS AuthKit provides hosted authentication UI with OAuth callback handling. Users are redirected to WorkOS for sign-in/sign-up, then returned to the callback route with session tokens.
+
 ## Configuration
 
 ### Environment Variables
@@ -192,10 +194,12 @@ CONVEX_DEPLOYMENT
 # Code Execution
 E2B_API_KEY
 
-# Authentication (Clerk)
-NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY
-CLERK_SECRET_KEY
-CLERK_JWT_ISSUER_DOMAIN  # From Clerk Dashboard → JWT Templates
+# Authentication (WorkOS)
+WORKOS_API_KEY                    # From WorkOS Dashboard → API Keys
+WORKOS_CLIENT_ID                  # From WorkOS Dashboard → Configuration
+WORKOS_REDIRECT_URI               # Auth callback URL (e.g., http://localhost:3000/callback)
+WORKOS_WEBHOOK_SECRET             # From WorkOS Dashboard → Webhooks
+WORKOS_API_URL                    # WorkOS API endpoint (https://api.workos.com)
 
 # File Upload (UploadThing)
 UPLOADTHING_TOKEN  # Get from https://uploadthing.com/dashboard

File: README.md
Changes:
@@ -11,7 +11,7 @@ AI-powered development platform that lets you create web applications by chattin
 - 💬 Conversational project development with message history
 - 🎯 Smart usage tracking and rate limiting
 - 💳 Subscription management with pro features
-- 🔐 Authentication with Clerk
+- 🔐 Authentication with WorkOS AuthKit
 - ⚙️ Background job processing with Inngest
 - 🗃️ Project management and persistence
 
@@ -23,11 +23,11 @@ AI-powered development platform that lets you create web applications by chattin
 - Tailwind CSS v4
 - Shadcn/ui
 - tRPC
-- Prisma ORM
-- PostgreSQL
+- Convex (real-time database)
+- tRPC (type-safe APIs)
 - Vercel AI Gateway (supports OpenAI, Anthropic, Grok, and more)
 - E2B Code Interpreter
-- Clerk Authentication
+- WorkOS AuthKit
 - Inngest
 - Prisma
 - Radix UI
@@ -142,13 +142,12 @@ AI_GATEWAY_BASE_URL="https://ai-gateway.vercel.sh/v1/"
 # E2B
 E2B_API_KEY=""
 
-# Clerk
-NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=""
-CLERK_SECRET_KEY=""
-NEXT_PUBLIC_CLERK_SIGN_IN_URL="/sign-in"
-NEXT_PUBLIC_CLERK_SIGN_UP_URL="/sign-up"
-NEXT_PUBLIC_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL="/"
-NEXT_PUBLIC_CLERK_SIGN_UP_FALLBACK_REDIRECT_URL="/"
+# WorkOS Authentication
+WORKOS_API_KEY=""                    # From WorkOS Dashboard → API Keys
+WORKOS_CLIENT_ID=""                  # From WorkOS Dashboard → Configuration
+WORKOS_REDIRECT_URI="http://localhost:3000/callback"  # Auth callback URL
+WORKOS_WEBHOOK_SECRET=""             # From WorkOS Dashboard → Webhooks
+WORKOS_API_URL="https://api.workos.com"
 
 # Inngest (for background job processing)
 INNGEST_EVENT_KEY=""

File: bun.lock
Changes:
@@ -5,7 +5,6 @@
     "": {
       "name": "vibe",
       "dependencies": {
-        "@clerk/nextjs": "^6.35.5",
         "@databuddy/sdk": "^2.2.1",
         "@e2b/code-interpreter": "^1.5.1",
         "@hookform/resolvers": "^3.3.4",
@@ -52,6 +51,7 @@
         "@typescript/native-preview": "^7.0.0-dev.20251104.1",
         "@uploadthing/react": "^7.3.3",
         "@vercel/speed-insights": "^1.2.0",
+        "@workos-inc/authkit-nextjs": "^0.14.0",
         "canvas-confetti": "^1.9.4",
         "class-variance-authority": "^0.7.1",
         "claude": "^0.1.2",
@@ -192,16 +192,10 @@
 
     "@bufbuild/protobuf": ["@bufbuild/protobuf@2.9.0", "", {}, "sha512-rnJenoStJ8nvmt9Gzye8nkYd6V22xUAnu4086ER7h1zJ508vStko4pMvDeQ446ilDTFpV5wnoc5YS7XvMwwMqA=="],
 
-    "@clerk/backend": ["@clerk/backend@2.24.0", "", { "dependencies": { "@clerk/shared": "^3.36.0", "@clerk/types": "^4.101.3", "cookie": "1.0.2", "standardwebhooks": "^1.0.0", "tslib": "2.8.1" } }, "sha512-6If+zmUiNEWVt5MoTjCl+0xiAq+uRR5kj+6HBXDG75KwRrG0eW3gH43QntQYBQU/SqhGwY2UbogEplM8ndF7Fg=="],
-
     "@clerk/clerk-react": ["@clerk/clerk-react@5.57.0", "", { "dependencies": { "@clerk/shared": "^3.36.0", "tslib": "2.8.1" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-0", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-0" } }, "sha512-GCBFF03HjEWvx58myjauJ7NrwTqhxHdetjWWxVM3YJGPOsAVXg4WuquL/hyn8KDuduCYSkRin4Hg6+QVP1NXAg=="],
 
-    "@clerk/nextjs": ["@clerk/nextjs@6.35.5", "", { "dependencies": { "@clerk/backend": "^2.24.0", "@clerk/clerk-react": "^5.57.0", "@clerk/shared": "^3.36.0", "@clerk/types": "^4.101.3", "server-only": "0.0.1", "tslib": "2.8.1" }, "peerDependencies": { "next": "^13.5.7 || ^14.2.25 || ^15.2.3 || ^16", "react": "^18.0.0 || ^19.0.0 || ^19.0.0-0", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-0" } }, "sha512-UmVSrfvMpkAU4ky+bam9bSc/Uvbv0XKR+KMWGWBKiGxsMluQI9Xt2bm7kPoxhHvTUnUQzP3BFgkbEgwIKeL4nQ=="],
-
     "@clerk/shared": ["@clerk/shared@3.36.0", "", { "dependencies": { "csstype": "3.1.3", "dequal": "2.0.3", "glob-to-regexp": "0.4.1", "js-cookie": "3.0.5", "std-env": "^3.9.0", "swr": "2.3.4" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-0", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-Yp4tL/x/iVft40DnxBjT/g/kQilZ+i9mYrqC1Lk6fUnfZV8t7E54GX19JtJSSONzjHsH6sCv3BmJaF1f7Eomkw=="],
 
-    "@clerk/types": ["@clerk/types@4.101.3", "", { "dependencies": { "@clerk/shared": "^3.36.0" } }, "sha512-QkYSiR8EDjLhQ3K9aCZ323knzZQggzhi3qxSdFrtI/C8Osyytua3Bu4TOGGRgYSSD4VO3s8WUz3wQf4Qe0ps/g=="],
-
     "@connectrpc/connect": ["@connectrpc/connect@2.0.0-rc.3", "", { "peerDependencies": { "@bufbuild/protobuf": "^2.2.0" } }, "sha512-ARBt64yEyKbanyRETTjcjJuHr2YXorzQo0etyS5+P6oSeW8xEuzajA9g+zDnMcj1hlX2dQE93foIWQGfpru7gQ=="],
 
     "@connectrpc/connect-web": ["@connectrpc/connect-web@2.0.0-rc.3", "", { "peerDependencies": { "@bufbuild/protobuf": "^2.2.0", "@connectrpc/connect": "2.0.0-rc.3" } }, "sha512-w88P8Lsn5CCsA7MFRl2e6oLY4J/5toiNtJns/YJrlyQaWOy3RO8pDgkz+iIkG98RPMhj2thuBvsd3Cn4DKKCkw=="],
@@ -624,6 +618,12 @@
 
     "@opentelemetry/sql-common": ["@opentelemetry/sql-common@0.41.2", "", { "dependencies": { "@opentelemetry/core": "^2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0" } }, "sha512-4mhWm3Z8z+i508zQJ7r6Xi7y4mmoJpdvH0fZPFRkWrdp5fq7hhZ2HhYokEOLkfqSMgPR4Z9EyB3DBkbKGOqZiQ=="],
 
+    "@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.6.0", "", { "dependencies": { "asn1js": "^3.0.6", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg=="],
+
+    "@peculiar/json-schema": ["@peculiar/json-schema@1.1.12", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w=="],
+
+    "@peculiar/webcrypto": ["@peculiar/webcrypto@1.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.3.8", "@peculiar/json-schema": "^1.1.12", "pvtsutils": "^1.3.5", "tslib": "^2.6.2", "webcrypto-core": "^1.8.0" } }, "sha512-BRs5XUAwiyCDQMsVA9IDvDa7UBR9gAvPHgugOeGng3YN6vJ9JYonyDc0lNczErgtCWtucjR5N7VtaonboD/ezg=="],
+
     "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
 
     "@pkgr/core": ["@pkgr/core@0.2.9", "", {}, "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA=="],
@@ -914,6 +914,8 @@
 
     "@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="],
 
+    "@types/accepts": ["@types/accepts@1.3.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ=="],
+
     "@types/aws-lambda": ["@types/aws-lambda@8.10.155", "", {}, "sha512-wd1XgoL0gy/ybo7WozUKQBd+IOgUkdfG6uUGI0fQOTEq06FBFdO7tmPDSxgjkFkl8GlfApvk5TvqZlAl0g+Lbg=="],
 
     "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
@@ -924,12 +926,20 @@
 
     "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
 
+    "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
+
     "@types/bunyan": ["@types/bunyan@1.8.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-758fRH7umIMk5qt5ELmRMff4mLDlN+xyYzC+dkPTdKwbSkJFvz6xwyScrytPU0QIBbRRwbiE8/BIg8bpajerNQ=="],
 
     "@types/canvas-confetti": ["@types/canvas-confetti@1.9.0", "", {}, "sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg=="],
 
     "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
 
+    "@types/content-disposition": ["@types/content-disposition@0.5.9", "", {}, "sha512-8uYXI3Gw35MhiVYhG3s295oihrxRyytcRHjSjqnqZVDDy/xcGBRny7+Xj1Wgfhv5QzRtN2hB2dVRBUX9XW3UcQ=="],
+
+    "@types/cookie": ["@types/cookie@0.5.4", "", {}, "sha512-7z/eR6O859gyWIAjuvBWFzNURmf2oPBmJlfVWkwehU5nzIyjwBsTh7WMmEEV4JFnHuQ3ex4oyTvfKzcyJVDBNA=="],
+
+    "@types/cookies": ["@types/cookies@0.9.2", "", { "dependencies": { "@types/connect": "*", "@types/express": "*", "@types/keygrip": "*", "@types/node": "*" } }, "sha512-1AvkDdZM2dbyFybL4fxpuNCaWyv//0AwsuUk2DWeXyM1/5ZKm6W3z6mQi24RZ4l2ucY+bkSHzbDVpySqPGuV8A=="],
+
     "@types/d3-array": ["@types/d3-array@3.2.1", "", {}, "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg=="],
 
     "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="],
@@ -958,8 +968,16 @@
 
     "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
 
+    "@types/express": ["@types/express@4.17.25", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "^1" } }, "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw=="],
+
+    "@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.7", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg=="],
+
     "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
 
+    "@types/http-assert": ["@types/http-assert@1.5.6", "", {}, "sha512-TTEwmtjgVbYAzZYWyeHPrrtWnfVkm8tQkP8P21uQifPgMRgjrow3XDEYqucuC8SKZJT7pUnhU/JymvjggxO9vw=="],
+
+    "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="],
+
     "@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="],
 
     "@types/istanbul-lib-report": ["@types/istanbul-lib-report@3.0.3", "", { "dependencies": { "@types/istanbul-lib-coverage": "*" } }, "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA=="],
@@ -970,10 +988,18 @@
 
     "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
 
+    "@types/keygrip": ["@types/keygrip@1.0.6", "", {}, "sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ=="],
+
+    "@types/koa": ["@types/koa@2.15.0", "", { "dependencies": { "@types/accepts": "*", "@types/content-disposition": "*", "@types/cookies": "*", "@types/http-assert": "*", "@types/http-errors": "*", "@types/keygrip": "*", "@types/koa-compose": "*", "@types/node": "*" } }, "sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g=="],
+
+    "@types/koa-compose": ["@types/koa-compose@3.2.9", "", { "dependencies": { "@types/koa": "*" } }, "sha512-BroAZ9FTvPiCy0Pi8tjD1OfJ7bgU1gQf0eR6e1Vm+JJATy9eKOG3hQMFtMciMawiSOVnLMdmUOC46s7HBhSTsA=="],
+
     "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
 
     "@types/memcached": ["@types/memcached@2.2.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-AM9smvZN55Gzs2wRrqeMHVP7KE8KWgCJO/XL5yCly2xF6EKa4YlbpK+cLSAH4NG/Ah64HrlegmGqW8kYws7Vxg=="],
 
+    "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="],
+
     "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
 
     "@types/mysql": ["@types/mysql@2.15.27", "", { "dependencies": { "@types/node": "*" } }, "sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA=="],
@@ -988,10 +1014,18 @@
 
     "@types/prismjs": ["@types/prismjs@1.26.5", "", {}, "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ=="],
 
+    "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="],
+
+    "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
+
     "@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
 
     "@types/react-dom": ["@types/react-dom@19.2.2", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw=="],
 
+    "@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="],
+
+    "@types/serve-static": ["@types/serve-static@1.15.10", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="],
+
     "@types/shimmer": ["@types/shimmer@1.2.0", "", {}, "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg=="],
 
     "@types/stack-utils": ["@types/stack-utils@2.0.3", "", {}, "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="],
@@ -1118,6 +1152,10 @@
 
     "@webassemblyjs/wast-printer": ["@webassemblyjs/wast-printer@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw=="],
 
+    "@workos-inc/authkit-nextjs": ["@workos-inc/authkit-nextjs@0.14.0", "", { "dependencies": { "@workos-inc/node": "^7.29.0", "iron-session": "^8.0.1", "jose": "^5.2.3", "path-to-regexp": "^6.2.2" }, "peerDependencies": { "next": "^13.5.4 || ^14.0.3 || ^15.0.0", "react": "^18.0 || ^19.0.0", "react-dom": "^18.0 || ^19.0.0" } }, "sha512-6hBBw4HPq00aP4jO0DHuF4gO6U+MLCeJLJ+eJSRqNSw0rhojJLbmNNyG55XH2OgNhKk2qomSlqBqurFnFq7lZA=="],
+
+    "@workos-inc/node": ["@workos-inc/node@7.74.2", "", { "dependencies": { "iron-session": "~6.3.1", "jose": "~5.6.3", "leb": "^1.0.0", "qs": "6.14.0" } }, "sha512-uXLEUqNo7AkM8oBzVPY29uyEpU0r8Jt2SppL2KzwFWB3f0hLLzp51wrIQ4PNJgd9qI5L/+kP62SoZFc8byI+4g=="],
+
     "@xtuc/ieee754": ["@xtuc/ieee754@1.2.0", "", {}, "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA=="],
 
     "@xtuc/long": ["@xtuc/long@4.2.2", "", {}, "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="],
@@ -1172,6 +1210,8 @@
 
     "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="],
 
+    "asn1js": ["asn1js@3.0.6", "", { "dependencies": { "pvtsutils": "^1.3.6", "pvutils": "^1.1.3", "tslib": "^2.8.1" } }, "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA=="],
+
     "ast-types-flow": ["ast-types-flow@0.0.8", "", {}, "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ=="],
 
     "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
@@ -1200,6 +1240,8 @@
 
     "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
 
+    "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
+
     "baseline-browser-mapping": ["baseline-browser-mapping@2.8.14", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-GM9c0cWWR8Ga7//Ves/9KRgTS8nLausCkP3CGiFLrnwA2CDUluXgaQqvrULoR2Ujrd/mz/lkX87F5BHFsNr5sQ=="],
 
     "bignumber.js": ["bignumber.js@9.3.0", "", {}, "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA=="],
@@ -1218,6 +1260,8 @@
 
     "bser": ["bser@2.1.1", "", { "dependencies": { "node-int64": "^0.4.0" } }, "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ=="],
 
+    "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
+
     "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
 
     "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
@@ -1302,7 +1346,7 @@
 
     "convex": ["convex@1.29.0", "", { "dependencies": { "esbuild": "0.25.4", "prettier": "^3.0.0" }, "peerDependencies": { "@auth0/auth0-react": "^2.0.1", "@clerk/clerk-react": "^4.12.8 || ^5.0.0", "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0" }, "optionalPeers": ["@auth0/auth0-react", "@clerk/clerk-react", "react"], "bin": { "convex": "bin/main.js" } }, "sha512-uoIPXRKIp2eLCkkR9WJ2vc9NtgQtx8Pml59WPUahwbrd5EuW2WLI/cf2E7XrUzOSifdQC3kJZepisk4wJNTJaA=="],
 
-    "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
+    "cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="],
 
     "cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="],
 
@@ -1660,6 +1704,8 @@
 
     "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
 
+    "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
+
     "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
 
     "immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="],
@@ -1688,6 +1734,10 @@
 
     "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
 
+    "iron-session": ["iron-session@8.0.4", "", { "dependencies": { "cookie": "^0.7.2", "iron-webcrypto": "^1.2.1", "uncrypto": "^0.1.3" } }, "sha512-9ivNnaKOd08osD0lJ3i6If23GFS2LsxyMU8Gf/uBUEgm8/8CC1hrrCHFDpMo3IFbpBgwoo/eairRsaD3c5itxA=="],
+
+    "iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="],
+
     "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="],
 
     "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="],
@@ -1870,6 +1920,8 @@
 
     "language-tags": ["language-tags@1.0.9", "", { "dependencies": { "language-subtag-registry": "^0.3.20" } }, "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA=="],
 
+    "leb": ["leb@1.0.0", "", {}, "sha512-Y3c3QZfvKWHX60BVOQPhLCvVGmDYWyJEiINE3drOog6KCyN2AOwvuQQzlS3uJg1J85kzpILXIUwRXULWavir+w=="],
+
     "leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="],
 
     "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
@@ -2120,7 +2172,7 @@
 
     "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
 
-    "path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="],
+    "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="],
 
     "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
 
@@ -2178,6 +2230,10 @@
 
     "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="],
 
+    "pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="],
+
+    "pvutils": ["pvutils@1.1.5", "", {}, "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA=="],
+
     "qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="],
 
     "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
@@ -2464,6 +2520,8 @@
 
     "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
 
+    "uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
+
     "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
 
     "unicode-emoji-modifier-base": ["unicode-emoji-modifier-base@1.0.0", "", {}, "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g=="],
@@ -2528,6 +2586,8 @@
 
     "web-vitals": ["web-vitals@5.1.0", "", {}, "sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg=="],
 
+    "webcrypto-core": ["webcrypto-core@1.8.1", "", { "dependencies": { "@peculiar/asn1-schema": "^2.3.13", "@peculiar/json-schema": "^1.1.12", "asn1js": "^3.0.5", "pvtsutils": "^1.3.5", "tslib": "^2.7.0" } }, "sha512-P+x1MvlNCXlKbLSOY4cYrdreqPG5hbzkmawbcXLKN/mf6DZW0SdNNkZ+sjwsqVkI4A4Ko2sPZmkZtCKY58w83A=="],
+
     "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
 
     "webpack": ["webpack@5.102.1", "", { "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.15.0", "acorn-import-phases": "^1.0.3", "browserslist": "^4.26.3", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.17.3", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", "terser-webpack-plugin": "^5.3.11", "watchpack": "^2.4.4", "webpack-sources": "^3.3.3" }, "bin": { "webpack": "bin/webpack.js" } }, "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ=="],
@@ -2952,6 +3012,14 @@
 
     "@typescript-eslint/typescript-estree/semver": ["semver@7.7.2", "", { "bin": "bin/semver.js" }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
 
+    "@workos-inc/authkit-nextjs/jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="],
+
+    "@workos-inc/node/iron-session": ["iron-session@6.3.1", "", { "dependencies": { "@peculiar/webcrypto": "^1.4.0", "@types/cookie": "^0.5.1", "@types/express": "^4.17.13", "@types/koa": "^2.13.5", "@types/node": "^17.0.41", "cookie": "^0.5.0", "iron-webcrypto": "^0.2.5" }, "peerDependencies": { "express": ">=4", "koa": ">=2", "next": ">=10" }, "optionalPeers": ["express", "koa", "next"] }, "sha512-3UJ7y2vk/WomAtEySmPgM6qtYF1cZ3tXuWX5GsVX4PJXAcs5y/sV9HuSfpjKS6HkTL/OhZcTDWJNLZ7w+Erx3A=="],
+
+    "@workos-inc/node/jose": ["jose@5.6.3", "", {}, "sha512-1Jh//hEEwMhNYPDDLwXHa2ePWgWiFNNUadVmguAAw2IJ6sj9mNxV5tGXJNqlMkJAybF6Lgw1mISDxTePP/187g=="],
+
+    "@workos-inc/node/qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
+
     "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
 
     "ajv-formats/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
@@ -2994,10 +3062,10 @@
 
     "execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
 
-    "express/cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="],
-
     "express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
 
+    "express/path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="],
+
     "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
 
     "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
@@ -3020,6 +3088,8 @@
 
     "inngest/zod": ["zod@4.1.12", "", {}, "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ=="],
 
+    "iron-session/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
+
     "is-bun-module/semver": ["semver@7.7.2", "", { "bin": "bin/semver.js" }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
 
     "istanbul-lib-instrument/semver": ["semver@7.7.2", "", { "bin": "bin/semver.js" }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
@@ -3198,8 +3268,6 @@
 
     "@modelcontextprotocol/sdk/express/content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="],
 
-    "@modelcontextprotocol/sdk/express/cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="],
-
     "@modelcontextprotocol/sdk/express/cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
 
     "@modelcontextprotocol/sdk/express/finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="],
@@ -3436,6 +3504,12 @@
 
     "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
 
+    "@workos-inc/node/iron-session/@types/node": ["@types/node@17.0.45", "", {}, "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw=="],
+
+    "@workos-inc/node/iron-session/cookie": ["cookie@0.5.0", "", {}, "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw=="],
+
+    "@workos-inc/node/iron-session/iron-webcrypto": ["iron-webcrypto@0.2.8", "", { "dependencies": { "buffer": "^6" } }, "sha512-YPdCvjFMOBjXaYuDj5tiHst5CEk6Xw84Jo8Y2+jzhMceclAnb3+vNPP/CTtb5fO2ZEuXEaO4N+w62Vfko757KA=="],
+
     "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
 
     "ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],

File: convex/auth.config.ts
Changes:
@@ -1,7 +1,7 @@
 export default {
   providers: [
     {
-      domain: process.env.CLERK_FRONTEND_API_URL,
+      domain: process.env.WORKOS_API_URL || "https://api.workos.com",
       applicationID: "convex",
     },
   ],

File: explanations/WORKOS_MIGRATION_GUIDE.md
Changes:
@@ -0,0 +1,271 @@
+# WorkOS Migration Guide
+
+This guide helps you migrate from Clerk to WorkOS AuthKit authentication.
+
+## Overview
+
+We've migrated from Clerk to WorkOS AuthKit for authentication. WorkOS provides enterprise-ready authentication with better pricing (free up to 1M users) and seamless integration with Convex.
+
+## What Changed
+
+### 1. Authentication Provider
+- **Before**: Clerk (`@clerk/nextjs`)
+- **After**: WorkOS AuthKit (`@workos-inc/authkit-nextjs`)
+
+### 2. Environment Variables
+
+**Remove these Clerk variables:**
+```bash
+NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY
+CLERK_SECRET_KEY
+CLERK_JWT_ISSUER_DOMAIN
+CLERK_WEBHOOK_SECRET
+NEXT_PUBLIC_CLERK_SIGN_IN_URL
+NEXT_PUBLIC_CLERK_SIGN_UP_URL
+NEXT_PUBLIC_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL
+NEXT_PUBLIC_CLERK_SIGN_UP_FALLBACK_REDIRECT_URL
+```
+
+**Add these WorkOS variables:**
+```bash
+WORKOS_API_KEY=sk_test_xxx                           # From WorkOS Dashboard → API Keys
+WORKOS_CLIENT_ID=client_xxx                          # From WorkOS Dashboard → Configuration
+WORKOS_REDIRECT_URI=http://localhost:3000/callback   # Auth callback URL
+WORKOS_WEBHOOK_SECRET=wh_secret_xxx                  # From WorkOS Dashboard → Webhooks
+WORKOS_API_URL=https://api.workos.com                # WorkOS API endpoint
+```
+
+### 3. Files Changed
+
+**Modified:**
+- `src/middleware.ts` - Uses WorkOS middleware
+- `src/components/convex-provider.tsx` - Integrates WorkOS with Convex
+- `src/lib/auth-server.ts` - Uses WorkOS server functions
+- `src/components/user-control.tsx` - Uses WorkOS user hooks
+- `src/app/sign-in/[[...sign-in]]/page.tsx` - Redirects to WorkOS
+- `src/app/sign-up/[[...sign-up]]/page.tsx` - Redirects to WorkOS
+- `src/modules/home/ui/components/navbar.tsx` - Updated auth buttons
+- `convex/auth.config.ts` - Points to WorkOS JWKS endpoint
+
+**Created:**
+- `src/app/callback/route.ts` - OAuth callback handler
+- `src/app/sign-out/route.ts` - Sign out handler
+- `src/app/api/webhooks/workos/route.ts` - WorkOS webhook handler
+
+**Deleted:**
+- `src/lib/clerk-config.ts` - Clerk configuration (no longer needed)
+- `src/app/api/webhooks/clerk/route.ts` - Clerk webhook handler
+
+## Setup Instructions
+
+### Step 1: Create WorkOS Account
+
+1. Go to [WorkOS Dashboard](https://dashboard.workos.com)
+2. Sign up for a free account
+3. Create a new environment (Development)
+
+### Step 2: Configure AuthKit
+
+1. In WorkOS Dashboard, go to **AuthKit** → **Configuration**
+2. Set up your redirect URI:
+   - Development: `http://localhost:3000/callback`
+   - Production: `https://your-domain.com/callback`
+3. Copy your **Client ID**
+
+### Step 3: Get API Keys
+
+1. Go to **API Keys** in WorkOS Dashboard
+2. Copy your **API Key** (starts with `sk_`)
+3. Store securely in your `.env.local` file
+
+### Step 4: Set Up Webhooks
+
+1. Go to **Webhooks** in WorkOS Dashboard
+2. Create a new webhook endpoint:
+   - Development: Use ngrok or similar tunneling service
+   - Production: `https://your-domain.com/api/webhooks/workos`
+3. Subscribe to these events:
+   - `user.created`
+   - `user.updated`
+   - `user.deleted`
+4. Copy the **Webhook Secret**
+
+### Step 5: Update Environment Variables
+
+Create or update your `.env.local` file:
+
+```bash
+# WorkOS Authentication
+WORKOS_API_KEY=sk_test_YOUR_API_KEY
+WORKOS_CLIENT_ID=client_YOUR_CLIENT_ID
+WORKOS_REDIRECT_URI=http://localhost:3000/callback
+WORKOS_WEBHOOK_SECRET=wh_secret_YOUR_WEBHOOK_SECRET
+WORKOS_API_URL=https://api.workos.com
+```
+
+### Step 6: Update Convex Deployment
+
+If you're using Convex, update the environment variables:
+
+```bash
+npx convex env set WORKOS_API_URL https://api.workos.com
+```
+
+Or set it in your Convex Dashboard → Settings → Environment Variables.
+
+### Step 7: Install Dependencies
+
+```bash
+bun install
+```
+
+This will install `@workos-inc/authkit-nextjs` and remove `@clerk/nextjs`.
+
+### Step 8: Test Authentication Flow
+
+1. Start your development server:
+   ```bash
+   bun run dev
+   ```
+
+2. Start Convex:
+   ```bash
+   bun run convex:dev
+   ```
+
+3. Navigate to `http://localhost:3000`
+
+4. Click "Sign in" - you should be redirected to WorkOS AuthKit
+
+5. Create a test account and verify:
+   - You're redirected back to your app
+   - User data appears in user control dropdown
+   - You can access protected routes
+
+## User Data Mapping
+
+WorkOS user object differs from Clerk:
+
+| Clerk | WorkOS |
+|-------|--------|
+| `user.fullName` | `[user.firstName, user.lastName].join(' ')` |
+| `user.primaryEmailAddress.emailAddress` | `user.email` |
+| `user.imageUrl` | `user.profilePictureUrl` |
+| `user.id` | `user.id` |
+
+## Migration Considerations
+
+### User IDs
+- Existing users have Clerk-formatted user IDs in your database
+- WorkOS generates different user ID formats
+- You may need to:
+  1. Add a migration script to map old Clerk IDs to new WorkOS IDs
+  2. Maintain a mapping table during transition period
+  3. Ask existing users to re-authenticate
+
+### Social Logins
+- Reconfigure OAuth apps (Google, GitHub, etc.) in WorkOS Dashboard
+- Update redirect URIs in each OAuth provider to point to WorkOS
+
+### Custom Claims
+- Clerk: Used `publicMetadata` for custom claims (e.g., `plan: "pro"`)
+- WorkOS: Uses organization metadata for custom claims
+- Current implementation: Plan is stored in Convex `subscriptions` table
+
+### Session Management
+- WorkOS sessions work differently from Clerk
+- Session tokens are managed automatically by WorkOS SDK
+- No need to manually refresh tokens
+
+## Troubleshooting
+
+### Issue: "Missing WORKOS_API_KEY"
+**Solution**: Ensure all WorkOS environment variables are set in `.env.local`
+
+### Issue: Redirect loop after sign-in
+**Solution**: Verify `WORKOS_REDIRECT_URI` matches exactly what's configured in WorkOS Dashboard
+
+### Issue: "Invalid session"
+**Solution**: 
+1. Clear browser cookies
+2. Verify `WORKOS_API_URL` is set correctly
+3. Check Convex `auth.config.ts` points to WorkOS
+
+### Issue: Webhook not receiving events
+**Solution**:
+1. For local development, use ngrok: `ngrok http 3000`
+2. Update webhook URL in WorkOS Dashboard
+3. Verify `WORKOS_WEBHOOK_SECRET` matches
+
+### Issue: User data not showing
+**Solution**: 
+1. Check browser console for errors
+2. Verify `useUser()` hook returns data
+3. Ensure session is valid
+
+## Production Deployment
+
+### Vercel Deployment
+
+1. Add all WorkOS environment variables to Vercel:
+   ```bash
+   vercel env add WORKOS_API_KEY
+   vercel env add WORKOS_CLIENT_ID
+   vercel env add WORKOS_REDIRECT_URI
+   vercel env add WORKOS_WEBHOOK_SECRET
+   vercel env add WORKOS_API_URL
+   ```
+
+2. Update `WORKOS_REDIRECT_URI` to production URL:
+   ```
+   https://your-domain.com/callback
+   ```
+
+3. Update WorkOS webhook endpoint to production:
+   ```
+   https://your-domain.com/api/webhooks/workos
+   ```
+
+4. Deploy:
+   ```bash
+   vercel --prod
+   ```
+
+### Convex Production
+
+Update Convex production environment:
+
+```bash
+npx convex deploy
+npx convex env set WORKOS_API_URL https://api.workos.com --prod
+```
+
+## Benefits of WorkOS
+
+1. **Cost**: Free up to 1M users (vs Clerk's pricing)
+2. **Enterprise Ready**: Built-in support for SSO, SCIM, RBAC
+3. **Convex Integration**: Official integration with Convex
+4. **Developer Experience**: Simple API, excellent docs
+5. **Flexibility**: Easy to customize authentication flows
+
+## Support
+
+- [WorkOS Documentation](https://workos.com/docs)
+- [WorkOS Next.js Guide](https://workos.com/docs/authkit/nextjs)
+- [Convex + WorkOS Integration](https://docs.convex.dev/auth/authkit)
+
+## Migration Checklist
+
+- [ ] Create WorkOS account
+- [ ] Configure AuthKit in WorkOS Dashboard
+- [ ] Get API keys and webhook secret
+- [ ] Update `.env.local` with WorkOS variables
+- [ ] Run `bun install`
+- [ ] Test sign-in/sign-up flow locally
+- [ ] Verify user data displays correctly
+- [ ] Test protected routes
+- [ ] Set up webhooks for production
+- [ ] Update Vercel environment variables
+- [ ] Deploy to production
+- [ ] Test production authentication flow
+- [ ] Monitor for errors in Sentry/logs

File: package.json
Changes:
@@ -12,7 +12,7 @@
     "convex:deploy": "bunx convex deploy"
   },
   "dependencies": {
-    "@clerk/nextjs": "^6.35.5",
+    "@workos-inc/authkit-nextjs": "^0.14.0",
     "@databuddy/sdk": "^2.2.1",
     "@e2b/code-interpreter": "^1.5.1",
     "@hookform/resolvers": "^3.3.4",

File: src/app/api/webhooks/clerk/route.ts
Changes:
@@ -1,94 +0,0 @@
-import { Webhook } from "svix";
-import { headers } from "next/headers";
-import { WebhookEvent } from "@clerk/nextjs/server";
-import { ConvexHttpClient } from "convex/browser";
-import { api } from "@/convex/_generated/api";
-
-export async function POST(req: Request) {
-    // You can find this in the Clerk Dashboard -> Webhooks -> choose the webhook
-    const WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET;
-
-    if (!WEBHOOK_SECRET) {
-        throw new Error(
-            "Please add CLERK_WEBHOOK_SECRET from Clerk Dashboard to .env or .env.local"
-        );
-    }
-
-    // Get the headers
-    const headerPayload = await headers();
-    const svix_id = headerPayload.get("svix-id");
-    const svix_timestamp = headerPayload.get("svix-timestamp");
-    const svix_signature = headerPayload.get("svix-signature");
-
-    // If there are no headers, error out
-    if (!svix_id || !svix_timestamp || !svix_signature) {
-        return new Response("Error occured -- no svix headers", {
-            status: 400,
-        });
-    }
-
-    // Get the body
-    const payload = await req.json();
-    const body = JSON.stringify(payload);
-
-    // Create a new Svix instance with your secret.
-    const wh = new Webhook(WEBHOOK_SECRET);
-
-    let evt: WebhookEvent;
-
-    // Verify the payload with the headers
-    try {
-        evt = wh.verify(body, {
-            "svix-id": svix_id,
-            "svix-timestamp": svix_timestamp,
-            "svix-signature": svix_signature,
-        }) as WebhookEvent;
-    } catch (err) {
-        console.error("Error verifying webhook:", err);
-        return new Response("Error occured", {
-            status: 400,
-        });
-    }
-
-    // Get the event type
-    const eventType = evt.type;
-
-    // Initialize Convex client
-    const convex = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
-
-    if (eventType === "user.created") {
-        const { id, email_addresses, first_name, last_name, image_url } = evt.data;
-
-        const email = email_addresses[0]?.email_address;
-        const name = `${first_name || ""} ${last_name || ""}`.trim();
-
-        console.log(`User created: ${id} (${email})`);
-
-        // Initialize usage for the new user
-        // This ensures they have free tier credits immediately
-        try {
-            await convex.mutation(api.usage.checkAndConsumeCreditForUser, {
-                userId: id,
-            });
-            // We actually just want to initialize, not consume. 
-            // But checkAndConsumeCreditForUser consumes 1 credit.
-            // Let's use a specific initialization mutation if possible, or just accept the 1 credit cost (unideal).
-            // Looking at usage.ts, there isn't a pure "init" mutation exposed.
-            // However, `getUsageForUser` calls `getUsageInternal` which does NOT create the record if missing (it just returns default values).
-            // `checkAndConsumeCreditInternal` creates it if missing.
-
-            // Let's rely on lazy initialization for now as per the existing design in `usage.ts`.
-            // The `usage.ts` logic says: "If no usage record or expired, create/reset with max points" inside `checkAndConsumeCredit`.
-            // So we don't strictly NEED to do anything here for usage.
-            // But we might want to store the user in a `users` table if we had one. 
-            // Since we don't have a `users` table (as per schema review), we just log it.
-
-            console.log(`User ${id} created. Usage will be initialized lazily on first generation.`);
-
-        } catch (error) {
-            console.error("Error initializing user data:", error);
-        }
-    }
-
-    return new Response("", { status: 200 });
-}

File: src/app/api/webhooks/workos/route.ts
Changes:
@@ -0,0 +1,68 @@
+import { headers } from "next/headers";
+import { ConvexHttpClient } from "convex/browser";
+import { api } from "@/convex/_generated/api";
+
+export async function POST(req: Request) {
+  // Get WorkOS webhook secret
+  const WEBHOOK_SECRET = process.env.WORKOS_WEBHOOK_SECRET;
+
+  if (!WEBHOOK_SECRET) {
+    throw new Error(
+      "Please add WORKOS_WEBHOOK_SECRET from WorkOS Dashboard to .env or .env.local"
+    );
+  }
+
+  // Get the headers
+  const headerPayload = await headers();
+  const signature = headerPayload.get("workos-signature");
+
+  if (!signature) {
+    return new Response("Error occurred -- no WorkOS signature header", {
+      status: 400,
+    });
+  }
+
+  // Get the body
+  const payload = await req.json();
+
+  // TODO: Verify webhook signature using WorkOS SDK
+  // For now, we'll process the event without verification
+  // In production, you should verify the signature:
+  // const isValid = await verifyWorkOSWebhook(payload, signature, WEBHOOK_SECRET);
+  // if (!isValid) {
+  //   return new Response("Invalid signature", { status: 400 });
+  // }
+
+  const eventType = payload.event;
+
+  // Initialize Convex client
+  const convex = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
+
+  if (eventType === "user.created") {
+    const { id, email, first_name, last_name, profile_picture_url } = payload.data;
+
+    console.log(`User created: ${id} (${email})`);
+
+    // Initialize usage for the new user
+    try {
+      // The usage system will initialize lazily on first generation
+      console.log(`User ${id} created. Usage will be initialized lazily on first generation.`);
+    } catch (error) {
+      console.error("Error initializing user data:", error);
+    }
+  }
+
+  if (eventType === "user.updated") {
+    const { id, email } = payload.data;
+    console.log(`User updated: ${id} (${email})`);
+    // Handle user updates if needed
+  }
+
+  if (eventType === "user.deleted") {
+    const { id } = payload.data;
+    console.log(`User deleted: ${id}`);
+    // Handle user deletion if needed (cleanup projects, messages, etc.)
+  }
+
+  return new Response("", { status: 200 });
+}

File: src/app/callback/route.ts
Changes:
@@ -0,0 +1,3 @@
+import { handleAuth } from "@workos-inc/authkit-nextjs";
+
+export const GET = handleAuth();

File: src/app/sign-in/[[...sign-in]]/page.tsx
Changes:
@@ -1,9 +1,7 @@
-import { SignIn } from "@clerk/nextjs";
+import { getSignInUrl } from "@workos-inc/authkit-nextjs";
+import { redirect } from "next/navigation";
 
-export default function Page() {
-    return (
-        <div className="flex h-screen w-full items-center justify-center">
-            <SignIn />
-        </div>
-    );
+export default async function SignInPage() {
+  const signInUrl = await getSignInUrl();
+  redirect(signInUrl);
 }

File: src/app/sign-out/route.ts
Changes:
@@ -0,0 +1,5 @@
+import { signOut } from "@workos-inc/authkit-nextjs";
+
+export async function GET() {
+  return await signOut();
+}

File: src/app/sign-up/[[...sign-up]]/page.tsx
Changes:
@@ -1,9 +1,7 @@
-import { SignUp } from "@clerk/nextjs";
+import { getSignUpUrl } from "@workos-inc/authkit-nextjs";
+import { redirect } from "next/navigation";
 
-export default function Page() {
-    return (
-        <div className="flex h-screen w-full items-center justify-center">
-            <SignUp />
-        </div>
-    );
+export default async function SignUpPage() {
+  const signUpUrl = await getSignUpUrl();
+  redirect(signUpUrl);
 }

File: src/components/convex-provider.tsx
Changes:
@@ -1,32 +1,36 @@
 "use client";
 
-import { ReactNode } from "react";
+import { ReactNode, useMemo } from "react";
 import { ConvexReactClient } from "convex/react";
-import { ClerkProvider, useAuth } from "@clerk/nextjs";
-import { ConvexProviderWithClerk } from "convex/react-clerk";
+import { ConvexProviderWithAuth } from "convex/react";
+import { useSession } from "@workos-inc/authkit-nextjs";
 
 if (!process.env.NEXT_PUBLIC_CONVEX_URL) {
   throw new Error("Missing NEXT_PUBLIC_CONVEX_URL in your .env file");
 }
 
-if (!process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY) {
-  throw new Error("Missing NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY in your .env file");
-}
-
 const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL);
 
 export function ConvexClientProvider({ children }: { children: ReactNode }) {
   return (
-    <ClerkProvider 
-      publishableKey={process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY}
-      signInUrl="/sign-in"
-      signUpUrl="/sign-up"
-      signInFallbackRedirectUrl="/"
-      signUpFallbackRedirectUrl="/"
-    >
-      <ConvexProviderWithClerk client={convex} useAuth={useAuth}>
-        {children}
-      </ConvexProviderWithClerk>
-    </ClerkProvider>
+    <ConvexProviderWithAuth client={convex} useAuth={useWorkOSAuth}>
+      {children}
+    </ConvexProviderWithAuth>
+  );
+}
+
+function useWorkOSAuth() {
+  const { session } = useSession();
+  
+  return useMemo(
+    () => ({
+      isLoading: false,
+      isAuthenticated: session !== null,
+      fetchAccessToken: async ({ forceRefreshToken }: { forceRefreshToken: boolean }) => {
+        if (!session) return null;
+        return session.accessToken;
+      },
+    }),
+    [session]
   );
 }

File: src/components/user-control.tsx
Changes:
@@ -1,6 +1,6 @@
 "use client";
 
-import { useUser, useClerk } from "@clerk/nextjs";
+import { useUser } from "@workos-inc/authkit-nextjs";
 import { useRouter } from "next/navigation";
 import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
 import {
@@ -20,24 +20,23 @@ interface Props {
 export const UserControl = ({ showName }: Props) => {
   const router = useRouter();
   const { user } = useUser();
-  const { signOut } = useClerk();
 
   if (!user) return null;
 
   const handleSignOut = async () => {
-    await signOut();
-    router.push("/");
+    // WorkOS sign out - redirect to sign out endpoint
+    window.location.href = "/sign-out";
   };
 
-  const displayName = user.fullName || user.primaryEmailAddress?.emailAddress || "User";
+  const displayName = [user.firstName, user.lastName].filter(Boolean).join(" ") || user.email || "User";
   const initials = displayName
     ?.split(" ")
     .map((n) => n[0])
     .join("")
     .toUpperCase()
     .slice(0, 2) || "U";
 
-  const avatarSrc = user.imageUrl || undefined;
+  const avatarSrc = user.profilePictureUrl || undefined;
 
   return (
     <DropdownMenu>
@@ -57,7 +56,7 @@ export const UserControl = ({ showName }: Props) => {
           <div className="flex flex-col space-y-1">
             <p className="text-sm font-medium leading-none">{displayName}</p>
             <p className="text-xs leading-none text-muted-foreground">
-              {user.primaryEmailAddress?.emailAddress}
+              {user.email}
             </p>
           </div>
         </DropdownMenuLabel>

File: src/lib/auth-server.ts
Changes:
@@ -1,19 +1,18 @@
 import { ConvexHttpClient } from "convex/browser";
-import { auth, currentUser } from "@clerk/nextjs/server";
+import { getUser as getWorkOSUser, getAccessToken, getSignInUrl, getSignUpUrl, signOut } from "@workos-inc/authkit-nextjs";
 
 /**
- * Get the authenticated user from Clerk
+ * Get the authenticated user from WorkOS
  */
 export async function getUser() {
-  return await currentUser();
+  return await getWorkOSUser();
 }
 
 /**
  * Get the authentication token for Convex
  */
 export async function getToken() {
-  const { getToken } = await auth();
-  return await getToken({ template: "convex" });
+  return await getAccessToken();
 }
 
 /**
@@ -28,7 +27,7 @@ export async function getAuthHeaders() {
 }
 
 /**
- * Create a Convex HTTP client with Clerk authentication
+ * Create a Convex HTTP client with WorkOS authentication
  * Use this in API routes that need to call Convex
  */
 export async function getConvexClientWithAuth() {
@@ -47,3 +46,8 @@ export async function getConvexClientWithAuth() {
 
   return httpClient;
 }
+
+/**
+ * Get WorkOS sign in URL
+ */
+export { getSignInUrl, getSignUpUrl, signOut };

File: src/lib/clerk-config.ts
Changes:
@@ -1,117 +0,0 @@
-/**
- * Centralizes Clerk-related environment handling with sensible fallbacks.
- * - Accepts either publishableKey or frontendApi (host only, no protocol).
- * - Normalizes optional proxy/sign-in URLs and trims stray slashes/protocols.
- * - Validates proxy URLs to prevent CORS/404 errors from invalid domains.
- */
-export function getClerkInstanceConfig() {
-  const publishableKey =
-    process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY ||
-    process.env.CLERK_PUBLISHABLE_KEY ||
-    process.env.VITE_CLERK_PUBLISHABLE_KEY ||
-    undefined;
-
-  const rawFrontendApi =
-    process.env.NEXT_PUBLIC_CLERK_FRONTEND_API ||
-    process.env.CLERK_FRONTEND_API_URL ||
-    process.env.NEXT_CLERK_FRONTEND_API_URL ||
-    process.env.CLERK_JWT_ISSUER_DOMAIN;
-  
-  const frontendApi = validateClerkDomain(rawFrontendApi);
-
-  if (!publishableKey && !frontendApi) {
-    throw new Error(
-      "Missing Clerk configuration. Set NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY or NEXT_PUBLIC_CLERK_FRONTEND_API",
-    );
-  }
-
-  return {
-    publishableKey,
-    frontendApi,
-    proxyUrl: validateProxyUrl(process.env.NEXT_PUBLIC_CLERK_PROXY_URL),
-    signInUrl:
-      normalizePath(process.env.NEXT_PUBLIC_CLERK_SIGN_IN_URL) || "/sign-in",
-    signUpUrl:
-      normalizePath(process.env.NEXT_PUBLIC_CLERK_SIGN_UP_URL) || "/sign-up",
-    signInFallbackRedirectUrl:
-      normalizePath(
-        process.env.NEXT_PUBLIC_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL,
-      ) || "/",
-    signUpFallbackRedirectUrl:
-      normalizePath(
-        process.env.NEXT_PUBLIC_CLERK_SIGN_UP_FALLBACK_REDIRECT_URL,
-      ) || "/",
-    appUrl: normalizeUrl(process.env.NEXT_PUBLIC_APP_URL),
-  };
-}
-
-function normalizeHost(value?: string | null) {
-  if (!value) return undefined;
-  return value.replace(/^https?:\/\//, "").replace(/\/$/, "");
-}
-
-function normalizeUrl(value?: string | null) {
-  if (!value) return undefined;
-  return value.replace(/\/$/, "");
-}
-
-function normalizePath(value?: string | null) {
-  if (!value) return undefined;
-  return value.startsWith("/") ? value : `/${value}`;
-}
-
-const VALID_CLERK_DOMAINS = [
-  ".clerk.accounts.dev",
-  ".clerk.dev",
-  ".clerkstage.dev",
-  ".lclclerk.com",
-  "clerk.zapdev.link",
-];
-
-function isValidClerkDomain(hostname: string): boolean {
-  const lower = hostname.toLowerCase();
-  return VALID_CLERK_DOMAINS.some(domain => 
-    domain.startsWith(".") ? lower.endsWith(domain) : lower === domain
-  );
-}
-
-function validateClerkDomain(value?: string | null): string | undefined {
-  if (!value) return undefined;
-
-  const normalized = normalizeHost(value);
-  if (!normalized) return undefined;
-
-  if (isValidClerkDomain(normalized)) {
-    return normalized;
-  }
-
-  console.warn(
-    `[Clerk Config] Invalid Clerk domain "${normalized}" - must be a valid Clerk domain (e.g., *.clerk.accounts.dev). Ignoring to prevent CORS/404 errors.`
-  );
-  return undefined;
-}
-
-function validateProxyUrl(value?: string | null): string | undefined {
-  if (!value) return undefined;
-  
-  const normalized = normalizeUrl(value);
-  if (!normalized) return undefined;
-  
-  try {
-    const url = new URL(normalized);
-    
-    if (!isValidClerkDomain(url.hostname)) {
-      console.warn(
-        `[Clerk Config] Invalid proxy URL "${normalized}" - must be a Clerk domain. Ignoring to prevent CORS/404 errors.`
-      );
-      return undefined;
-    }
-    
-    return normalized;
-  } catch {
-    console.warn(
-      `[Clerk Config] Invalid proxy URL format "${value}". Ignoring.`
-    );
-    return undefined;
-  }
-}

File: src/middleware.ts
Changes:
@@ -1,20 +1,20 @@
-import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
+import { authkitMiddleware } from "@workos-inc/authkit-nextjs";
 
-const isPublicRoute = createRouteMatcher([
-  "/",
-  "/sign-in(.*)",
-  "/sign-up(.*)",
-  "/api/webhooks(.*)",
-  "/api/inngest(.*)",
-  "/api/uploadthing(.*)",
-  "/api/vitals",
-  "/api/rss",
-]);
-
-export default clerkMiddleware(async (auth, request) => {
-  if (!isPublicRoute(request)) {
-    await auth.protect();
-  }
+export default authkitMiddleware({
+  middlewareAuth: {
+    enabled: true,
+    unauthenticatedPaths: [
+      "/",
+      "/sign-in",
+      "/sign-up",
+      "/pricing",
+      "/api/webhooks(.*)",
+      "/api/inngest(.*)",
+      "/api/uploadthing(.*)",
+      "/api/vitals",
+      "/api/rss",
+    ],
+  },
 });
 
 export const config = {

File: src/modules/home/ui/components/navbar.tsx
Changes:
@@ -6,7 +6,7 @@ import { cn } from "@/lib/utils";
 import { useScroll } from "@/hooks/use-scroll";
 import { Button } from "@/components/ui/button";
 import { UserControl } from "@/components/user-control";
-import { useUser, SignInButton, SignUpButton } from "@clerk/nextjs";
+import { useUser } from "@workos-inc/authkit-nextjs";
 import {
   NavigationMenu,
   NavigationMenuItem,
@@ -86,16 +86,12 @@ export const Navbar = () => {
             </DropdownMenu>
             {!user ? (
               <div className="flex gap-2">
-                <SignUpButton mode="modal">
-                  <Button variant="outline" size="sm">
-                    Sign up
-                  </Button>
-                </SignUpButton>
-                <SignInButton mode="modal">
-                  <Button size="sm">
-                    Sign in
-                  </Button>
-                </SignInButton>
+                <Button variant="outline" size="sm" asChild>
+                  <Link href="/sign-up">Sign up</Link>
+                </Button>
+                <Button size="sm" asChild>
+                  <Link href="/sign-in">Sign in</Link>
+                </Button>
               </div>
             ) : (
               <UserControl showName />

@vercel
Copy link

vercel bot commented Nov 28, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
zapdev Error Error Nov 28, 2025 2:42am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 28, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Replace Clerk authentication with WorkOS AuthKit across the app: dependencies, environment variables, middleware, auth pages/routes, webhook handling, Convex auth configuration, UI components, and documentation.

Changes

Cohort / File(s) Summary
Documentation
AGENTS.md, CLAUDE.md, README.md, explanations/WORKOS_MIGRATION_GUIDE.md
Replace Clerk references with WorkOS AuthKit; update env var lists to WORKOS_*; add WORKOS migration guide and update build/config notes.
Dependencies
package.json
Remove @clerk/nextjs; add @workos-inc/authkit-nextjs.
Convex auth config
convex/auth.config.ts
Replace Clerk domain-based provider with customJwt providers using WorkOS issuer, JWKS, RS256 algorithm and applicationID mapped from WORKOS_CLIENT_ID.
Webhook routes
src/app/api/webhooks/clerk/route.ts, src/app/api/webhooks/workos/route.ts
Remove Clerk/Svix webhook route; add WorkOS webhook POST handler validating WORKOS_WEBHOOK_SECRET, verifying WorkOS signature, and handling user.created/updated/deleted.
OAuth callback route
src/app/callback/route.ts
Add GET callback route using handleAuth() from WorkOS AuthKit.
Auth pages / sign-out
src/app/sign-in/[[...sign-in]]/page.tsx, src/app/sign-up/[[...sign-up]]/page.tsx, src/app/sign-out/route.ts
Convert sign-in/sign-up pages to server-side redirects via getSignInUrl() / getSignUpUrl() and add sign-out route calling WorkOS signOut() then redirecting.
Auth provider & hooks
src/components/convex-provider.tsx, src/lib/auth-server.ts
Replace Clerk provider/hooks with WorkOS-based auth (useAccessToken/getUser/getAccessToken exports); update Convex provider to use WorkOS auth token fetching and export sign-in/up/signOut helpers.
Middleware
src/middleware.ts
Replace clerkMiddleware and route-matcher logic with authkitMiddleware configured via middlewareAuth.unauthenticatedPaths.
UI / user components
src/components/user-control.tsx, src/modules/home/ui/components/navbar.tsx
Swap Clerk user APIs/components for WorkOS useAuth(); adjust displayed fields (firstName/lastName, profilePictureUrl, email); replace Clerk sign-in/up buttons with Link-based buttons and sign-out redirect.
Removed Clerk config
src/lib/clerk-config.ts
Delete Clerk-specific environment config, normalization, and domain validation utilities.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant User
    participant Browser
    participant NextApp
    participant WorkOS
    participant Convex
    Note over Browser,NextApp: Sign-in redirect flow
    User->>Browser: Click "Sign in"
    Browser->>NextApp: GET /sign-in
    NextApp->>WorkOS: request sign-in URL (getSignInUrl)
    WorkOS-->>NextApp: sign-in URL (redirect target)
    NextApp-->>Browser: 302 redirect to WorkOS hosted UI
    Note over WorkOS,NextApp: OAuth callback
    WorkOS->>Browser: OAuth callback -> /callback?code=...
    Browser->>NextApp: GET /callback
    NextApp->>WorkOS: handleAuth() exchanges code for token, obtains user
    WorkOS-->>NextApp: access token + user info
    NextApp->>Convex: (optional) initialize or attach auth token for server-side calls
    Convex-->>NextApp: ack
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Areas needing focused review:
    • convex/auth.config.ts: correctness of issuer/JWKS URLs, RS256 configuration, and applicationID mapping.
    • src/app/api/webhooks/workos/route.ts: raw-body parsing, signature verification correctness, and payload field mappings for user events.
    • src/lib/auth-server.ts and src/components/convex-provider.tsx: token lifecycles, fetchAccessToken behavior, and Convex client auth header handling.
    • src/middleware.ts: ensure unauthenticatedPaths cover intended public routes and edge cases.
    • Sign-in/sign-up redirect logic: confirm no redirect loops and correct redirect URIs.

Possibly related PRs

Suggested labels

scout

Suggested reviewers

  • dogesman098

Poem

🐰 A hop from Clerk to WorkOS I made,
Redirects, webhooks, and keys in parade,
Tokens exchanged with a confident clap,
Convex awaits on the authentication map,
Hooray — the rabbit rewired the auth glade! 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: Migrate authentication from Clerk to WorkOS AuthKit' accurately summarizes the primary change in the changeset—a comprehensive authentication provider migration.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/workos-migration

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecapyai
Copy link

codecapyai bot commented Nov 28, 2025

🚀 Scrapybara Ubuntu instance started!

Interactive stream

⚠️ Error fetching GitHub variables, continuing setup:

status_code: 502, body: {'detail': "Error communicating with container: Client error '400 Bad Request' for url 'http://54.176.2.220:30067/env'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400"}

@codecapyai
Copy link

codecapyai bot commented Nov 28, 2025

🔧 Setting up test environment...

Agent Steps

@codecapyai
Copy link

codecapyai bot commented Nov 28, 2025

❌ Something went wrong:

status_code: 502, body: {'detail': {'error': "Unexpected error: Error code: 404 - {'type': 'error', 'error': {'type': 'not_found_error', 'message': 'model: claude-3-5-sonnet-20241022'}, 'request_id': 'req_011CVZTRTb9W39oD4U5ZUXy8'}", 'provider': 'anthropic', 'error_type': 'ProviderAPIError'}}

@claude
Copy link

claude bot commented Nov 28, 2025

PR Review: Migration from Clerk to WorkOS AuthKit

Overview

This PR successfully migrates authentication from Clerk to WorkOS AuthKit. The migration is well-structured with comprehensive documentation and follows best practices. However, there are several critical security issues and improvements needed before merging.


🚨 Critical Security Issues

1. Missing Webhook Signature Verification (HIGH PRIORITY)

File: src/app/api/webhooks/workos/route.ts:28-34

The webhook handler currently has signature verification commented out with a TODO:

// TODO: Verify webhook signature using WorkOS SDK
// For now, we'll process the event without verification

Risk: Without signature verification, anyone can send fake webhook events to your endpoint, potentially:

  • Creating fake user accounts
  • Triggering unauthorized actions
  • Manipulating user data

Recommendation: Implement signature verification immediately using WorkOS SDK or standard webhook signature validation:

import { Webhook } from 'standardwebhooks';

const webhook = new Webhook(WEBHOOK_SECRET);
try {
  const payload = webhook.verify(await req.text(), {
    'workos-signature': signature,
  });
  // Process verified payload
} catch (err) {
  return new Response("Invalid signature", { status: 400 });
}

2. Convex Client Without Authentication in Webhook

File: src/app/api/webhooks/workos/route.ts:39

The webhook creates an unauthenticated Convex client, which may fail if Convex enforces authentication for all operations.

Recommendation: If Convex operations require auth, consider using service account credentials or ensure the webhook can perform necessary operations without auth.


🔒 Security Concerns

3. Missing Environment Variable Validation

The webhook throws an error if WORKOS_WEBHOOK_SECRET is missing, but only at runtime. Consider validating all required environment variables at startup.

Recommendation: Create an environment validation file (e.g., src/lib/env.ts) that validates all required variables at build/startup time using Zod:

import { z } from 'zod';

const envSchema = z.object({
  WORKOS_API_KEY: z.string().min(1),
  WORKOS_CLIENT_ID: z.string().min(1),
  WORKOS_REDIRECT_URI: z.string().url(),
  WORKOS_WEBHOOK_SECRET: z.string().min(1),
  // ... other vars
});

export const env = envSchema.parse(process.env);

4. Client-Side Sign-Out Implementation

File: src/components/user-control.tsx:26-29

Using window.location.href for sign-out is not ideal for SPA behavior.

Recommendation: Use Next.js router for better UX:

import { useRouter } from 'next/navigation';

const router = useRouter();
const handleSignOut = () => {
  router.push('/sign-out');
};

🐛 Potential Bugs

5. Unused Variables in Webhook Handler

File: src/app/api/webhooks/workos/route.ts:42-52

Variables first_name, last_name, and profile_picture_url are destructured but never used. The comment suggests lazy initialization, but these fields might be needed for future features.

Recommendation: Either remove unused variables or add a comment explaining they're reserved for future use.

6. Missing Error Handling in Auth Hook

File: src/components/convex-provider.tsx:22-36

The useWorkOSAuth hook doesn't handle potential errors from fetchAccessToken. If the session is in an invalid state, this could cause silent failures.

Recommendation: Add error handling:

fetchAccessToken: async ({ forceRefreshToken }: { forceRefreshToken: boolean }) => {
  try {
    if (!session) return null;
    return session.accessToken;
  } catch (error) {
    console.error('Failed to fetch access token:', error);
    return null;
  }
},

💡 Code Quality Improvements

7. Hard-Coded isLoading State

File: src/components/convex-provider.tsx:27

isLoading: false is hard-coded, which might cause UI issues if the session is actually loading.

Recommendation: Track actual loading state from WorkOS:

const { session, isLoading: sessionLoading } = useSession();

return useMemo(
  () => ({
    isLoading: sessionLoading,
    // ...
  }),
  [session, sessionLoading]
);

8. Missing Type Safety in Webhook Payload

File: src/app/api/webhooks/workos/route.ts

The webhook payload is typed as any (implicitly). Should use Zod or TypeScript interfaces for type safety.

Recommendation:

import { z } from 'zod';

const userEventSchema = z.object({
  event: z.enum(['user.created', 'user.updated', 'user.deleted']),
  data: z.object({
    id: z.string(),
    email: z.string().email(),
    first_name: z.string().optional(),
    last_name: z.string().optional(),
    profile_picture_url: z.string().url().optional(),
  }),
});

const payload = userEventSchema.parse(await req.json());

9. Inconsistent Error Handling

The webhook has try-catch for user creation but not for other events. Error handling should be consistent.


📚 Documentation & Testing

10. Excellent Migration Guide

The WORKOS_MIGRATION_GUIDE.md is comprehensive and well-structured. Great job!

Minor suggestion: Add a troubleshooting section for common issues:

  • What to do if webhooks aren't being received
  • How to verify WorkOS configuration is correct
  • Testing webhooks locally with ngrok

11. Missing Tests

No tests were added for the new authentication flow.

Recommendation: Add tests for:

  • Webhook signature verification (when implemented)
  • Auth middleware behavior
  • User session handling

🎯 Best Practices

12. Convex Auth Config

File: convex/auth.config.ts

The domain uses process.env.WORKOS_API_URL with a fallback. Good practice! ✅

Minor improvement: Consider validating this is a valid URL.

13. Middleware Configuration

The middleware correctly excludes API routes, webhooks, and public paths. Well done!


⚡ Performance Considerations

14. useMemo Optimization

Good use of useMemo in useWorkOSAuth to prevent unnecessary re-renders.

15. Webhook Response Time

The webhook handler performs synchronous operations that could be slow. Consider using background jobs for heavy operations.

Recommendation: For user deletion cleanup, use Inngest to handle in background:

if (eventType === "user.deleted") {
  await inngest.send({
    name: "user/cleanup",
    data: { userId: id }
  });
}

📦 Dependencies

16. Dependency Changes

  • Removed @clerk/nextjs correctly
  • Added @workos-inc/authkit-nextjs@^0.14.0
  • Lock file updated properly

Note: The bun.lock shows version discrepancies in transitive dependencies (multiple versions of jose, cookie, etc.). This is normal but monitor for potential issues.


🔍 Testing Checklist

Before merging, ensure you've tested:

  • Sign-up flow works correctly
  • Sign-in flow works correctly
  • Sign-out flow works correctly
  • Callback route handles OAuth properly
  • Middleware protects authenticated routes
  • Webhook receives events from WorkOS (use ngrok for local testing)
  • Convex queries work with WorkOS JWT tokens
  • User data is correctly displayed in UI
  • Session persistence across page refreshes
  • Error states are handled gracefully

📋 Summary

Strengths:

  • Well-structured migration with minimal breaking changes
  • Comprehensive documentation
  • Clean code organization
  • Proper use of React hooks and memoization

Must Fix Before Merge:

  1. ❗ Implement webhook signature verification
  2. ❗ Add proper error handling
  3. ❗ Validate environment variables at startup
  4. ❗ Add type safety to webhook payloads

Nice to Have:

  • Add tests for authentication flow
  • Implement proper loading states
  • Add troubleshooting section to docs
  • Consider background job processing for webhooks

Overall Assessment: This is a solid migration, but the missing webhook signature verification is a critical security vulnerability that must be addressed before merging to production.


🎖️ Recommendation

Do not merge until webhook signature verification is implemented. Once that's addressed, this will be ready for staging deployment and testing.

Great work on the migration overall! The documentation and code structure are excellent. Just need to close those security gaps. 🚀

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Nov 28, 2025

Greptile Overview

Greptile Summary

This PR migrates authentication from Clerk to WorkOS AuthKit, replacing the authentication provider, middleware, and user management across the application.

Key Changes:

  • Replaced @clerk/nextjs with @workos-inc/authkit-nextjs in dependencies
  • Updated middleware to use WorkOS authkitMiddleware with unauthenticated paths configuration
  • Migrated Convex provider to integrate with WorkOS sessions via custom useWorkOSAuth hook
  • Created OAuth callback route (/callback) and sign-out route (/sign-out) for WorkOS flow
  • Updated all authentication server helpers to use WorkOS SDK functions
  • Configured Convex auth.config.ts to validate JWTs against WorkOS JWKS endpoint
  • Added comprehensive migration guide in explanations/WORKOS_MIGRATION_GUIDE.md

Critical Issues Found:

  • Incomplete migration: 4 component files still import @clerk/nextjs instead of WorkOS (projects-list.tsx, message-form.tsx, project-view.tsx, subscription/page.tsx)
  • Security vulnerability: WorkOS webhook handler lacks signature verification, making it susceptible to spoofed requests
  • Race condition potential: ConvexProvider hardcodes isLoading: false, which may cause authentication state issues during initial page load

Benefits:
The migration to WorkOS provides better pricing (free up to 1M users vs Clerk's paid tiers) and enterprise-ready authentication with seamless Convex integration.

Confidence Score: 2/5

  • This PR has critical issues that must be resolved before merging
  • Score reflects incomplete migration with 4 files still importing Clerk, webhook endpoint missing signature verification (security risk), and potential race condition in auth provider. These issues will cause runtime errors and security vulnerabilities.
  • Pay close attention to src/app/api/webhooks/workos/route.ts (missing signature verification), src/components/convex-provider.tsx (hardcoded isLoading), and the 4 files with remaining Clerk imports: src/modules/home/ui/components/projects-list.tsx, src/modules/projects/ui/components/message-form.tsx, src/modules/projects/ui/views/project-view.tsx, and src/app/dashboard/subscription/page.tsx

Important Files Changed

File Analysis

Filename Score Overview
src/middleware.ts 4/5 replaced Clerk middleware with WorkOS authkitMiddleware, updated public route configuration
convex/auth.config.ts 4/5 updated JWKS provider domain from Clerk to WorkOS API URL
src/lib/auth-server.ts 4/5 migrated authentication helpers from Clerk to WorkOS, simplified token retrieval
src/components/convex-provider.tsx 3/5 integrated WorkOS session with Convex provider, hardcoded isLoading: false may cause race conditions
src/app/api/webhooks/workos/route.ts 1/5 created WorkOS webhook handler without signature verification (security vulnerability)
src/components/user-control.tsx 4/5 migrated from Clerk to WorkOS user hook, updated user property access

Sequence Diagram

sequenceDiagram
    participant User
    participant Browser
    participant Middleware as Next.js Middleware
    participant WorkOS as WorkOS AuthKit
    participant Callback as /callback Route
    participant Convex as Convex Backend
    
    Note over User,Convex: Sign-In Flow
    User->>Browser: Click "Sign in"
    Browser->>Middleware: Navigate to /sign-in
    Middleware->>WorkOS: Redirect to WorkOS hosted UI
    WorkOS->>User: Display authentication form
    User->>WorkOS: Enter credentials
    WorkOS->>Callback: Redirect to /callback with code
    Callback->>WorkOS: Exchange code for session
    WorkOS->>Callback: Return access token & user data
    Callback->>Browser: Set session cookie & redirect
    Browser->>Convex: Request with session token
    Convex->>WorkOS: Verify JWT via JWKS endpoint
    WorkOS->>Convex: Return user identity
    Convex->>Browser: Authenticated response
    
    Note over User,Convex: Protected Route Access
    Browser->>Middleware: Request protected route
    Middleware->>WorkOS: Validate session
    alt Session Valid
        Middleware->>Browser: Allow access
        Browser->>Convex: Query with access token
        Convex->>WorkOS: Verify JWT
        Convex->>Browser: Return user data
    else Session Invalid
        Middleware->>WorkOS: Redirect to sign-in
    end
    
    Note over User,Convex: Sign-Out Flow
    User->>Browser: Click "Sign out"
    Browser->>Callback: Navigate to /sign-out
    Callback->>WorkOS: Clear session
    WorkOS->>Browser: Redirect to home page
Loading

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional Comments (4)

  1. src/modules/home/ui/components/projects-list.tsx, line 5 (link)

    syntax: still imports @clerk/nextjs instead of @workos-inc/authkit-nextjs

  2. src/modules/projects/ui/components/message-form.tsx, line 14 (link)

    syntax: still imports @clerk/nextjs instead of @workos-inc/authkit-nextjs

  3. src/modules/projects/ui/views/project-view.tsx, line 9 (link)

    syntax: still imports @clerk/nextjs instead of @workos-inc/authkit-nextjs

  4. src/app/dashboard/subscription/page.tsx, line 5 (link)

    syntax: still imports @clerk/nextjs instead of @workos-inc/authkit-nextjs

18 files reviewed, 6 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 27 to 34

// TODO: Verify webhook signature using WorkOS SDK
// For now, we'll process the event without verification
// In production, you should verify the signature:
// const isValid = await verifyWorkOSWebhook(payload, signature, WEBHOOK_SECRET);
// if (!isValid) {
// return new Response("Invalid signature", { status: 400 });
// }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: webhook signature verification is commented out, leaving the endpoint vulnerable to spoofed requests. implement verification using WorkOS SDK before deploying to production

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/api/webhooks/workos/route.ts
Line: 27:34

Comment:
**logic:** webhook signature verification is commented out, leaving the endpoint vulnerable to spoofed requests. implement verification using WorkOS SDK before deploying to production

How can I resolve this? If you propose a fix, please make it concise.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/components/user-control.tsx (1)

3-37: Fix incorrect WorkOS hook import and add type annotation to map callback

The useUser hook doesn't exist in @workos-inc/authkit-nextjs. The correct export is useSession. Additionally, you need to type the map callback parameter to satisfy strict mode.

Proposed fix:

-import { useUser } from "@workos-inc/authkit-nextjs";
+import { useSession } from "@workos-inc/authkit-nextjs";
 import { useRouter } from "next/navigation";
 import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
 import {
@@
 export const UserControl = ({ showName }: Props) => {
   const router = useRouter();
-  const { user } = useUser();
+  const { session } = useSession();
+  const user = session?.user;

   if (!user) return null;
@@
   const displayName = [user.firstName, user.lastName].filter(Boolean).join(" ") || user.email || "User";
   const initials = displayName
     ?.split(" ")
-    .map((n) => n[0])
+    .map((n: string) => n[0])

Also update src/modules/home/ui/components/navbar.tsx line 9 with the same import correction.

src/lib/auth-server.ts (1)

2-48: Perfect! I now have complete verification. Let me finalize my findings:

Key Findings:

  1. getUser() IS a valid server export in v0.14.0 and works correctly
  2. getAccessToken() does NOT exist as a server-side export in v0.14.0 (it's only available as a client hook via useAccessToken)
  3. getToken() is actually called in the codebase (in getAuthHeaders() and getConvexClientWithAuth())
  4. withAuth() is a valid alternative that provides both user and accessToken

Verdict: The review comment is partially incorrect in its diagnosis. It flags both getUser and getAccessToken as non-existent, but only getAccessToken is actually broken. However, the suggested fix using withAuth() is valid and will resolve the actual error.


Use withAuth() to obtain the access token; getUser() import is already correct

The import of getAccessToken is failing because it doesn't exist as a server-side export in @workos-inc/authkit-nextjs@0.14.0—it's only available as a client hook (useAccessToken). However, getUser() is a valid server export and works as-is.

The fix is to update only the getToken() function to use withAuth():

-import { getUser as getWorkOSUser, getAccessToken, getSignInUrl, getSignUpUrl, signOut } from "@workos-inc/authkit-nextjs";
+import { getUser as getWorkOSUser, withAuth, getSignInUrl, getSignUpUrl, signOut } from "@workos-inc/authkit-nextjs";
@@
 /**
  * Get the authentication token for Convex
  */
 export async function getToken() {
-  return await getAccessToken();
+  const { accessToken } = await withAuth();
+  return accessToken ?? null;
 }

The rest of the file (auth headers and Convex client wiring) can remain unchanged.

Make sure all call sites use these helpers only in server contexts where withAuth() can access the current session (server components, route handlers, etc.).

🧹 Nitpick comments (5)
package.json (1)

15-15: Verify @workos-inc/authkit-nextjs version against Next 16 support and recent security advisory

Adding AuthKit Next.js makes sense for the migration, but ^0.14.0 is an older line while:

  • Current WorkOS docs/examples (including Convex + AuthKit) assume the latest AuthKit Next.js APIs and env vars.
  • There is a recent CVE (CVE-2025-64762) affecting @workos-inc/authkit-nextjs releases up to 2.11.0, with a fix in 2.11.1.
  • Community reports have called out peer-dependency conflicts between some AuthKit Next.js versions and Next 16.

Please confirm that ^0.14.0 is:

  1. Explicitly supported with Next 16, and
  2. Either not affected by CVE-2025-64762 or upgraded to a patched line once Next 16 support is available (e.g., 2.11.1+).

Also ensure your env vars follow current AuthKit Next.js guidance (WORKOS_CLIENT_ID, WORKOS_API_KEY, WORKOS_COOKIE_PASSWORD, NEXT_PUBLIC_WORKOS_REDIRECT_URI) so the SDK picks them up correctly.

README.md (1)

14-31: Tech stack bullets look good, but tRPC is listed twice

The new “Authentication with WorkOS AuthKit” and “WorkOS AuthKit” tech‑stack bullets correctly reflect the migration.

Small nit: tRPC now appears twice in the Tech Stack list (generic - tRPC and - tRPC (type-safe APIs)), which is redundant. You can drop one of them to keep the list concise.

AGENTS.md (1)

40-44: Auth description is accurate; mirror full WorkOS env requirements here

The updated architecture line:

- **Auth**: WorkOS AuthKit with JWT authentication

matches the new auth stack and looks good.

In the “Environment Variables” section, you currently list the WorkOS vars as:

- WORKOS_API_KEY
- WORKOS_CLIENT_ID
- WORKOS_REDIRECT_URI
- WORKOS_WEBHOOK_SECRET

To keep this in sync with both the README and the AuthKit Next.js + Convex docs, I’d suggest:

  • Adding WORKOS_COOKIE_PASSWORD (≥32 chars, required for encrypted cookies).
  • Clarifying that the redirect env used by AuthKit Next.js is NEXT_PUBLIC_WORKOS_REDIRECT_URI (or documenting both if you intentionally use WORKOS_REDIRECT_URI elsewhere in server code).

Also, if you care about markdownlint’s MD034 warning, you can wrap bare URLs like https://ai-gateway.vercel.sh/v1/ in <...> or [link](...) to quiet the linter.

Also applies to: 120-132

explanations/WORKOS_MIGRATION_GUIDE.md (1)

50-58: Polish wording and fenced code blocks in the migration guide

Two small doc nits:

  • Around Line 52, consider hyphenating “Sign out handler” → “Sign‑out handler” for consistency.
  • Around Lines 220 and 225, the fenced code blocks lack a language spec. To satisfy MD040 and improve rendering, tag them, e.g.:
-  ```
+  ```text
   https://your-domain.com/callback

(and similarly for the webhook URL block).




Also applies to: 218-227

</blockquote></details>
<details>
<summary>src/middleware.ts (1)</summary><blockquote>

`1-18`: **Middleware config matches AuthKit’s middlewareAuth pattern; consider Next 16 `proxy.ts` guidance**

The `authkitMiddleware` usage with `middlewareAuth.enabled` and the `unauthenticatedPaths` list is in line with AuthKit’s documented “middleware auth” configuration. ([github.com](https://github.com/workos/authkit-nextjs))  

Since you’re on Next 16, AuthKit’s docs recommend using a `proxy.ts` entrypoint for middleware on 16+ (same code, different filename/function). It’s not strictly required for this PR, but worth double‑checking against your deployment setup.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used**: CodeRabbit UI

**Review profile**: CHILL

**Plan**: Pro

**Disabled knowledge base sources:**

- Linear integration is disabled by default for public repositories

> You can enable these sources in your CodeRabbit configuration.

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 6c0d6787e74981d7e55f7142d01b884968e79465 and c13163bee900902d4296c2997fbe4f6b4b61716d.

</details>

<details>
<summary>⛔ Files ignored due to path filters (2)</summary>

* `assets/image-7ce41e19-6014-4c28-a2d6-9ea1b063ad9d.png` is excluded by `!**/*.png`
* `bun.lock` is excluded by `!**/*.lock`

</details>

<details>
<summary>📒 Files selected for processing (18)</summary>

* `AGENTS.md` (2 hunks)
* `CLAUDE.md` (3 hunks)
* `README.md` (3 hunks)
* `convex/auth.config.ts` (1 hunks)
* `explanations/WORKOS_MIGRATION_GUIDE.md` (1 hunks)
* `package.json` (1 hunks)
* `src/app/api/webhooks/clerk/route.ts` (0 hunks)
* `src/app/api/webhooks/workos/route.ts` (1 hunks)
* `src/app/callback/route.ts` (1 hunks)
* `src/app/sign-in/[[...sign-in]]/page.tsx` (1 hunks)
* `src/app/sign-out/route.ts` (1 hunks)
* `src/app/sign-up/[[...sign-up]]/page.tsx` (1 hunks)
* `src/components/convex-provider.tsx` (1 hunks)
* `src/components/user-control.tsx` (3 hunks)
* `src/lib/auth-server.ts` (3 hunks)
* `src/lib/clerk-config.ts` (0 hunks)
* `src/middleware.ts` (1 hunks)
* `src/modules/home/ui/components/navbar.tsx` (2 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (2)</summary>

* src/app/api/webhooks/clerk/route.ts
* src/lib/clerk-config.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code graph analysis (6)</summary>

<details>
<summary>src/app/sign-out/route.ts (2)</summary><blockquote>

<details>
<summary>src/app/callback/route.ts (1)</summary>

* `GET` (3-3)

</details>
<details>
<summary>src/lib/auth-server.ts (1)</summary>

* `signOut` (53-53)

</details>

</blockquote></details>
<details>
<summary>src/app/api/webhooks/workos/route.ts (1)</summary><blockquote>

<details>
<summary>tests/mocks/convex-browser.ts (1)</summary>

* `ConvexHttpClient` (1-11)

</details>

</blockquote></details>
<details>
<summary>src/modules/home/ui/components/navbar.tsx (1)</summary><blockquote>

<details>
<summary>src/components/ui/button.tsx (1)</summary>

* `Button` (60-60)

</details>

</blockquote></details>
<details>
<summary>src/app/sign-up/[[...sign-up]]/page.tsx (1)</summary><blockquote>

<details>
<summary>src/lib/auth-server.ts (1)</summary>

* `getSignUpUrl` (53-53)

</details>

</blockquote></details>
<details>
<summary>src/app/sign-in/[[...sign-in]]/page.tsx (1)</summary><blockquote>

<details>
<summary>src/lib/auth-server.ts (1)</summary>

* `getSignInUrl` (53-53)

</details>

</blockquote></details>
<details>
<summary>src/app/callback/route.ts (1)</summary><blockquote>

<details>
<summary>src/app/sign-out/route.ts (1)</summary>

* `GET` (3-5)

</details>

</blockquote></details>

</details><details>
<summary>🪛 GitHub Actions: CI</summary>

<details>
<summary>src/components/user-control.tsx</summary>

[error] 3-3: Module "@workos-inc/authkit-nextjs" has no exported member 'useUser'.

---

[error] 34-34: Parameter 'n' implicitly has an 'any' type.

</details>
<details>
<summary>src/modules/home/ui/components/navbar.tsx</summary>

[error] 9-9: "@workos-inc/authkit-nextjs" has no exported member 'useUser'.

</details>
<details>
<summary>src/components/convex-provider.tsx</summary>

[error] 6-6: "@workos-inc/authkit-nextjs" has no exported member named 'useSession'. Did you mean 'getSession'?

</details>
<details>
<summary>src/lib/auth-server.ts</summary>

[error] 2-2: Module "@workos-inc/authkit-nextjs" has no exported member 'getUser'.

---

[error] 2-2: Module "@workos-inc/authkit-nextjs" has no exported member 'getAccessToken'.

</details>

</details>
<details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>explanations/WORKOS_MIGRATION_GUIDE.md</summary>

[grammar] ~52-~52: Use a hyphen to join words.
Context: ...ler - `src/app/sign-out/route.ts` - Sign out handler - `src/app/api/webhooks/work...

(QB_NEW_EN_HYPHEN)

</details>

</details>
<details>
<summary>🪛 markdownlint-cli2 (0.18.1)</summary>

<details>
<summary>AGENTS.md</summary>

128-128: Bare URL used

(MD034, no-bare-urls)

</details>
<details>
<summary>explanations/WORKOS_MIGRATION_GUIDE.md</summary>

220-220: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

---

225-225: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)</summary>

* GitHub Check: claude-review
* GitHub Check: Codacy Security Scan
* GitHub Check: Analyze (javascript-typescript)

</details>

<details>
<summary>🔇 Additional comments (3)</summary><blockquote>

<details>
<summary>src/app/callback/route.ts (1)</summary><blockquote>

`1-3`: **Callback handler matches AuthKit docs; just confirm redirect URI wiring**

Using:

```ts
import { handleAuth } from "@workos-inc/authkit-nextjs";

export const GET = handleAuth();

is exactly how the AuthKit Next.js docs and Convex + AuthKit examples define the callback route.

Please just double‑check that:

  • This file is located at /app/callback/route.ts (path /callback), and
  • Your WorkOS Dashboard “Redirect URI” and NEXT_PUBLIC_WORKOS_REDIRECT_URI (or equivalent) are set to http://localhost:3000/callback in dev and the matching URL in prod.
src/app/sign-in/[[...sign-in]]/page.tsx (1)

1-6: Sign-in redirect flow looks correct

The page cleanly delegates to WorkOS by fetching getSignInUrl() server-side and calling redirect(signInUrl), which matches the documented AuthKit pattern for sign-in redirects. (github.com)

src/app/sign-up/[[...sign-up]]/page.tsx (1)

1-6: Sign-up redirect flow is consistent and aligned with AuthKit usage

This page mirrors the sign-in behavior by resolving getSignUpUrl() server-side and calling redirect(signUpUrl), which is exactly how AuthKit expects you to kick off sign-up. (github.com)

@codecapyai
Copy link

codecapyai bot commented Nov 28, 2025

CodeCapy Review ₍ᐢ•(ܫ)•ᐢ₎

Codebase Summary

ZapDev is an AI-powered development platform built with Next.js, React, TypeScript, and Tailwind CSS. It includes features like real-time sandboxed development, AI code generation through Inngest and Vercel AI Gateway, and a real-time database with Convex. The recent changes migrate the authentication system from Clerk to WorkOS AuthKit, updating sign-in/sign-up flows, OAuth callback and sign-out routes, navigation components, middleware, and several configuration files.

PR Changes

The pull request replaces Clerk with WorkOS AuthKit for authentication. This includes changes in sign-in, sign-up, and sign-out pages/routes, middleware modifications, updates to user control and navbar components, and adjustments in server-side authentication helpers. The documentation and environment variable examples have been updated accordingly.

Setup Instructions

  1. Install pnpm globally by running: sudo npm install -g pnpm.
  2. Clone the repository and navigate into its directory.
  3. Run 'pnpm install' to install all dependencies.
  4. Copy .env.example to .env and fill in your WorkOS and other necessary environment variables.
  5. Start the development server by running: pnpm dev.
  6. Open your browser and navigate to http://localhost:3000 to interact with the application.

Generated Test Cases

1: WorkOS Sign In Redirect Flow ❗️❗️❗️

Description: This test verifies that when a user navigates to the sign in page (via /sign-in), they are automatically redirected to the WorkOS-hosted sign in page, reflecting the migration from Clerk to WorkOS AuthKit.

Prerequisites:

  • User is not signed in
  • The application is running locally
  • Environment variables for WorkOS are properly set

Steps:

  1. Open a browser and navigate to http://localhost:3000/sign-in.
  2. Observe that the page does not render an in-app sign in form but instead redirects to the WorkOS sign in URL provided by the WorkOS AuthKit.
  3. Confirm the redirected URL is consistent with WorkOS authentication service.

Expected Result: The browser is redirected to the WorkOS sign in page, and the URL reflects a WorkOS sign in URL (e.g., containing parameters or domain from WorkOS).

2: WorkOS Sign Up Redirect Flow ❗️❗️❗️

Description: This test confirms that navigating to the sign up page redirects users to the WorkOS sign up page, ensuring that the new authentication flow for sign-up is correctly integrated.

Prerequisites:

  • User is not signed in
  • The application is running locally
  • WorkOS environment variables configured

Steps:

  1. Open a browser and navigate to http://localhost:3000/sign-up.
  2. Observe that the page immediately redirects to the WorkOS sign up URL.
  3. Verify that the URL in the browser reflects the WorkOS sign up endpoint.

Expected Result: The browser is redirected to the WorkOS sign up page and the redirection occurs without any in-app sign up form rendering.

3: WorkOS Sign Out Flow ❗️❗️

Description: This test checks that when a signed-in user chooses to sign out, the system calls the WorkOS sign out functionality and the user is redirected to the home page.

Prerequisites:

  • User is signed in via WorkOS authentication
  • Application is running locally

Steps:

  1. Simulate a signed-in user state (this can be done through a test account or by manually setting a valid session).
  2. Navigate to a page that shows the user control (usually found in the navbar).
  3. Click the sign out option within the user control dropdown.
  4. Observe that the application calls the sign out route and the user is redirected to the home page.

Expected Result: The user is successfully signed out and redirected to the home page (http://localhost:3000), with no user-specific data displayed in the user control.

4: Navbar Authentication Buttons Display for Unauthenticated Users ❗️❗️

Description: This test verifies that the navigation bar displays the correct authentication buttons (Sign In and Sign Up) when no user is logged in.

Prerequisites:

  • User is not authenticated
  • Application is running locally

Steps:

  1. Open a browser and navigate to http://localhost:3000.
  2. Examine the navbar area at the top.
  3. Confirm that the navbar contains visible 'Sign in' and 'Sign up' buttons or links.
  4. Click each button to verify that it leads to the respective redirection pages (/sign-in and /sign-up).

Expected Result: The navbar should display both the 'Sign in' and 'Sign up' buttons. Clicking these buttons should navigate to /sign-in and /sign-up respectively, initiating the redirection flow to WorkOS.

5: User Control Display After Successful Authentication ❗️❗️❗️

Description: This test validates that once a user has successfully signed in via WorkOS, the user control in the navbar correctly displays the user’s name (combining first and last names) and email, and shows an avatar if available.

Prerequisites:

  • User is signed in via WorkOS authentication
  • A test account with firstName, lastName, email, and optionally profilePictureUrl is available
  • Application is running locally

Steps:

  1. Ensure the user is signed in by completing the WorkOS authentication process.
  2. Navigate to the home page (http://localhost:3000) after redirection back from WorkOS.
  3. Locate the user control dropdown in the navbar.
  4. Verify that the displayed user name is a combination of firstName and lastName (or falls back to email if name is not provided).
  5. Check that the email is displayed correctly and that the avatar image is shown if profilePictureUrl is available.

Expected Result: The user control should show the user's full name (constructed from first and last name) and email address accurately. If an avatar is provided, it should be visible; otherwise, a fallback avatar is displayed.

Raw Changes Analyzed
File: AGENTS.md
Changes:
@@ -39,7 +39,7 @@ e2b template build --name your-template-name --cmd "/compile_page.sh"
 ### Tech Stack
 - **Frontend**: Next.js 15 (App Router), React 19, TypeScript, Tailwind CSS v4, Shadcn/ui
 - **Backend**: Convex (real-time database), tRPC (type-safe APIs)
-- **Auth**: Clerk with JWT authentication
+- **Auth**: WorkOS AuthKit with JWT authentication
 - **AI**: Vercel AI Gateway (Claude via Anthropic), Inngest Agent Kit
 - **Code Execution**: E2B Code Interpreter (isolated sandboxes)
 - **Background Jobs**: Inngest
@@ -123,9 +123,10 @@ Required for development:
 - `AI_GATEWAY_API_KEY`: Vercel AI Gateway key
 - `AI_GATEWAY_BASE_URL`: https://ai-gateway.vercel.sh/v1/
 - `E2B_API_KEY`: E2B sandbox API key
-- `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY`: Clerk publishable key
-- `CLERK_SECRET_KEY`: Clerk secret key
-- `CLERK_JWT_ISSUER_DOMAIN`: Clerk JWT issuer domain (from dashboard)
+- `WORKOS_API_KEY`: WorkOS API key (from dashboard)
+- `WORKOS_CLIENT_ID`: WorkOS client ID (from dashboard)
+- `WORKOS_REDIRECT_URI`: Auth callback URL (e.g., http://localhost:3000/callback)
+- `WORKOS_WEBHOOK_SECRET`: WorkOS webhook secret (from dashboard)
 - `INNGEST_EVENT_KEY`: Inngest event key
 - `INNGEST_SIGNING_KEY`: Inngest signing key
 

File: CLAUDE.md
Changes:
@@ -10,7 +10,7 @@ ZapDev is an AI-powered development platform that enables users to create web ap
 
 **Frontend**: Next.js 15 (Turbopack), React 19, TypeScript 5.9, Tailwind CSS v4, Shadcn/ui, React Query
 **Backend**: Convex (real-time database), tRPC (type-safe APIs)
-**Authentication**: Clerk (user auth & JWT)
+**Authentication**: WorkOS AuthKit (user auth & JWT)
 **AI & Execution**: Vercel AI Gateway, Inngest 3.44 (job orchestration), E2B Code Interpreter (sandboxes)
 **Monitoring**: Sentry, OpenTelemetry
 
@@ -176,6 +176,8 @@ Subscriptions enable real-time UI updates when data changes.
 
 **Query Client**: React Query configured in `src/trpc/query-client.ts` for caching, refetching, and optimistic updates.
 
+**Authentication Flow**: WorkOS AuthKit provides hosted authentication UI with OAuth callback handling. Users are redirected to WorkOS for sign-in/sign-up, then returned to the callback route with session tokens.
+
 ## Configuration
 
 ### Environment Variables
@@ -192,10 +194,12 @@ CONVEX_DEPLOYMENT
 # Code Execution
 E2B_API_KEY
 
-# Authentication (Clerk)
-NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY
-CLERK_SECRET_KEY
-CLERK_JWT_ISSUER_DOMAIN  # From Clerk Dashboard → JWT Templates
+# Authentication (WorkOS)
+WORKOS_API_KEY                    # From WorkOS Dashboard → API Keys
+WORKOS_CLIENT_ID                  # From WorkOS Dashboard → Configuration
+WORKOS_REDIRECT_URI               # Auth callback URL (e.g., http://localhost:3000/callback)
+WORKOS_WEBHOOK_SECRET             # From WorkOS Dashboard → Webhooks
+WORKOS_API_URL                    # WorkOS API endpoint (https://api.workos.com)
 
 # File Upload (UploadThing)
 UPLOADTHING_TOKEN  # Get from https://uploadthing.com/dashboard

File: README.md
Changes:
@@ -11,7 +11,7 @@ AI-powered development platform that lets you create web applications by chattin
 - 💬 Conversational project development with message history
 - 🎯 Smart usage tracking and rate limiting
 - 💳 Subscription management with pro features
-- 🔐 Authentication with Clerk
+- 🔐 Authentication with WorkOS AuthKit
 - ⚙️ Background job processing with Inngest
 - 🗃️ Project management and persistence
 
@@ -23,11 +23,11 @@ AI-powered development platform that lets you create web applications by chattin
 - Tailwind CSS v4
 - Shadcn/ui
 - tRPC
-- Prisma ORM
-- PostgreSQL
+- Convex (real-time database)
+- tRPC (type-safe APIs)
 - Vercel AI Gateway (supports OpenAI, Anthropic, Grok, and more)
 - E2B Code Interpreter
-- Clerk Authentication
+- WorkOS AuthKit
 - Inngest
 - Prisma
 - Radix UI
@@ -142,13 +142,12 @@ AI_GATEWAY_BASE_URL="https://ai-gateway.vercel.sh/v1/"
 # E2B
 E2B_API_KEY=""
 
-# Clerk
-NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=""
-CLERK_SECRET_KEY=""
-NEXT_PUBLIC_CLERK_SIGN_IN_URL="/sign-in"
-NEXT_PUBLIC_CLERK_SIGN_UP_URL="/sign-up"
-NEXT_PUBLIC_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL="/"
-NEXT_PUBLIC_CLERK_SIGN_UP_FALLBACK_REDIRECT_URL="/"
+# WorkOS Authentication
+WORKOS_API_KEY=""                    # From WorkOS Dashboard → API Keys
+WORKOS_CLIENT_ID=""                  # From WorkOS Dashboard → Configuration
+WORKOS_REDIRECT_URI="http://localhost:3000/callback"  # Auth callback URL
+WORKOS_WEBHOOK_SECRET=""             # From WorkOS Dashboard → Webhooks
+WORKOS_API_URL="https://api.workos.com"
 
 # Inngest (for background job processing)
 INNGEST_EVENT_KEY=""

File: bun.lock
Changes:
@@ -5,7 +5,6 @@
     "": {
       "name": "vibe",
       "dependencies": {
-        "@clerk/nextjs": "^6.35.5",
         "@databuddy/sdk": "^2.2.1",
         "@e2b/code-interpreter": "^1.5.1",
         "@hookform/resolvers": "^3.3.4",
@@ -52,6 +51,7 @@
         "@typescript/native-preview": "^7.0.0-dev.20251104.1",
         "@uploadthing/react": "^7.3.3",
         "@vercel/speed-insights": "^1.2.0",
+        "@workos-inc/authkit-nextjs": "^0.14.0",
         "canvas-confetti": "^1.9.4",
         "class-variance-authority": "^0.7.1",
         "claude": "^0.1.2",
@@ -192,16 +192,10 @@
 
     "@bufbuild/protobuf": ["@bufbuild/protobuf@2.9.0", "", {}, "sha512-rnJenoStJ8nvmt9Gzye8nkYd6V22xUAnu4086ER7h1zJ508vStko4pMvDeQ446ilDTFpV5wnoc5YS7XvMwwMqA=="],
 
-    "@clerk/backend": ["@clerk/backend@2.24.0", "", { "dependencies": { "@clerk/shared": "^3.36.0", "@clerk/types": "^4.101.3", "cookie": "1.0.2", "standardwebhooks": "^1.0.0", "tslib": "2.8.1" } }, "sha512-6If+zmUiNEWVt5MoTjCl+0xiAq+uRR5kj+6HBXDG75KwRrG0eW3gH43QntQYBQU/SqhGwY2UbogEplM8ndF7Fg=="],
-
     "@clerk/clerk-react": ["@clerk/clerk-react@5.57.0", "", { "dependencies": { "@clerk/shared": "^3.36.0", "tslib": "2.8.1" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-0", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-0" } }, "sha512-GCBFF03HjEWvx58myjauJ7NrwTqhxHdetjWWxVM3YJGPOsAVXg4WuquL/hyn8KDuduCYSkRin4Hg6+QVP1NXAg=="],
 
-    "@clerk/nextjs": ["@clerk/nextjs@6.35.5", "", { "dependencies": { "@clerk/backend": "^2.24.0", "@clerk/clerk-react": "^5.57.0", "@clerk/shared": "^3.36.0", "@clerk/types": "^4.101.3", "server-only": "0.0.1", "tslib": "2.8.1" }, "peerDependencies": { "next": "^13.5.7 || ^14.2.25 || ^15.2.3 || ^16", "react": "^18.0.0 || ^19.0.0 || ^19.0.0-0", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-0" } }, "sha512-UmVSrfvMpkAU4ky+bam9bSc/Uvbv0XKR+KMWGWBKiGxsMluQI9Xt2bm7kPoxhHvTUnUQzP3BFgkbEgwIKeL4nQ=="],
-
     "@clerk/shared": ["@clerk/shared@3.36.0", "", { "dependencies": { "csstype": "3.1.3", "dequal": "2.0.3", "glob-to-regexp": "0.4.1", "js-cookie": "3.0.5", "std-env": "^3.9.0", "swr": "2.3.4" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-0", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-Yp4tL/x/iVft40DnxBjT/g/kQilZ+i9mYrqC1Lk6fUnfZV8t7E54GX19JtJSSONzjHsH6sCv3BmJaF1f7Eomkw=="],
 
-    "@clerk/types": ["@clerk/types@4.101.3", "", { "dependencies": { "@clerk/shared": "^3.36.0" } }, "sha512-QkYSiR8EDjLhQ3K9aCZ323knzZQggzhi3qxSdFrtI/C8Osyytua3Bu4TOGGRgYSSD4VO3s8WUz3wQf4Qe0ps/g=="],
-
     "@connectrpc/connect": ["@connectrpc/connect@2.0.0-rc.3", "", { "peerDependencies": { "@bufbuild/protobuf": "^2.2.0" } }, "sha512-ARBt64yEyKbanyRETTjcjJuHr2YXorzQo0etyS5+P6oSeW8xEuzajA9g+zDnMcj1hlX2dQE93foIWQGfpru7gQ=="],
 
     "@connectrpc/connect-web": ["@connectrpc/connect-web@2.0.0-rc.3", "", { "peerDependencies": { "@bufbuild/protobuf": "^2.2.0", "@connectrpc/connect": "2.0.0-rc.3" } }, "sha512-w88P8Lsn5CCsA7MFRl2e6oLY4J/5toiNtJns/YJrlyQaWOy3RO8pDgkz+iIkG98RPMhj2thuBvsd3Cn4DKKCkw=="],
@@ -624,6 +618,12 @@
 
     "@opentelemetry/sql-common": ["@opentelemetry/sql-common@0.41.2", "", { "dependencies": { "@opentelemetry/core": "^2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0" } }, "sha512-4mhWm3Z8z+i508zQJ7r6Xi7y4mmoJpdvH0fZPFRkWrdp5fq7hhZ2HhYokEOLkfqSMgPR4Z9EyB3DBkbKGOqZiQ=="],
 
+    "@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.6.0", "", { "dependencies": { "asn1js": "^3.0.6", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg=="],
+
+    "@peculiar/json-schema": ["@peculiar/json-schema@1.1.12", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w=="],
+
+    "@peculiar/webcrypto": ["@peculiar/webcrypto@1.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.3.8", "@peculiar/json-schema": "^1.1.12", "pvtsutils": "^1.3.5", "tslib": "^2.6.2", "webcrypto-core": "^1.8.0" } }, "sha512-BRs5XUAwiyCDQMsVA9IDvDa7UBR9gAvPHgugOeGng3YN6vJ9JYonyDc0lNczErgtCWtucjR5N7VtaonboD/ezg=="],
+
     "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
 
     "@pkgr/core": ["@pkgr/core@0.2.9", "", {}, "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA=="],
@@ -914,6 +914,8 @@
 
     "@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="],
 
+    "@types/accepts": ["@types/accepts@1.3.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ=="],
+
     "@types/aws-lambda": ["@types/aws-lambda@8.10.155", "", {}, "sha512-wd1XgoL0gy/ybo7WozUKQBd+IOgUkdfG6uUGI0fQOTEq06FBFdO7tmPDSxgjkFkl8GlfApvk5TvqZlAl0g+Lbg=="],
 
     "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
@@ -924,12 +926,20 @@
 
     "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
 
+    "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
+
     "@types/bunyan": ["@types/bunyan@1.8.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-758fRH7umIMk5qt5ELmRMff4mLDlN+xyYzC+dkPTdKwbSkJFvz6xwyScrytPU0QIBbRRwbiE8/BIg8bpajerNQ=="],
 
     "@types/canvas-confetti": ["@types/canvas-confetti@1.9.0", "", {}, "sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg=="],
 
     "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
 
+    "@types/content-disposition": ["@types/content-disposition@0.5.9", "", {}, "sha512-8uYXI3Gw35MhiVYhG3s295oihrxRyytcRHjSjqnqZVDDy/xcGBRny7+Xj1Wgfhv5QzRtN2hB2dVRBUX9XW3UcQ=="],
+
+    "@types/cookie": ["@types/cookie@0.5.4", "", {}, "sha512-7z/eR6O859gyWIAjuvBWFzNURmf2oPBmJlfVWkwehU5nzIyjwBsTh7WMmEEV4JFnHuQ3ex4oyTvfKzcyJVDBNA=="],
+
+    "@types/cookies": ["@types/cookies@0.9.2", "", { "dependencies": { "@types/connect": "*", "@types/express": "*", "@types/keygrip": "*", "@types/node": "*" } }, "sha512-1AvkDdZM2dbyFybL4fxpuNCaWyv//0AwsuUk2DWeXyM1/5ZKm6W3z6mQi24RZ4l2ucY+bkSHzbDVpySqPGuV8A=="],
+
     "@types/d3-array": ["@types/d3-array@3.2.1", "", {}, "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg=="],
 
     "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="],
@@ -958,8 +968,16 @@
 
     "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
 
+    "@types/express": ["@types/express@4.17.25", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "^1" } }, "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw=="],
+
+    "@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.7", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg=="],
+
     "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
 
+    "@types/http-assert": ["@types/http-assert@1.5.6", "", {}, "sha512-TTEwmtjgVbYAzZYWyeHPrrtWnfVkm8tQkP8P21uQifPgMRgjrow3XDEYqucuC8SKZJT7pUnhU/JymvjggxO9vw=="],
+
+    "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="],
+
     "@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="],
 
     "@types/istanbul-lib-report": ["@types/istanbul-lib-report@3.0.3", "", { "dependencies": { "@types/istanbul-lib-coverage": "*" } }, "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA=="],
@@ -970,10 +988,18 @@
 
     "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
 
+    "@types/keygrip": ["@types/keygrip@1.0.6", "", {}, "sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ=="],
+
+    "@types/koa": ["@types/koa@2.15.0", "", { "dependencies": { "@types/accepts": "*", "@types/content-disposition": "*", "@types/cookies": "*", "@types/http-assert": "*", "@types/http-errors": "*", "@types/keygrip": "*", "@types/koa-compose": "*", "@types/node": "*" } }, "sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g=="],
+
+    "@types/koa-compose": ["@types/koa-compose@3.2.9", "", { "dependencies": { "@types/koa": "*" } }, "sha512-BroAZ9FTvPiCy0Pi8tjD1OfJ7bgU1gQf0eR6e1Vm+JJATy9eKOG3hQMFtMciMawiSOVnLMdmUOC46s7HBhSTsA=="],
+
     "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
 
     "@types/memcached": ["@types/memcached@2.2.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-AM9smvZN55Gzs2wRrqeMHVP7KE8KWgCJO/XL5yCly2xF6EKa4YlbpK+cLSAH4NG/Ah64HrlegmGqW8kYws7Vxg=="],
 
+    "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="],
+
     "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
 
     "@types/mysql": ["@types/mysql@2.15.27", "", { "dependencies": { "@types/node": "*" } }, "sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA=="],
@@ -988,10 +1014,18 @@
 
     "@types/prismjs": ["@types/prismjs@1.26.5", "", {}, "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ=="],
 
+    "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="],
+
+    "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
+
     "@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
 
     "@types/react-dom": ["@types/react-dom@19.2.2", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw=="],
 
+    "@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="],
+
+    "@types/serve-static": ["@types/serve-static@1.15.10", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="],
+
     "@types/shimmer": ["@types/shimmer@1.2.0", "", {}, "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg=="],
 
     "@types/stack-utils": ["@types/stack-utils@2.0.3", "", {}, "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="],
@@ -1118,6 +1152,10 @@
 
     "@webassemblyjs/wast-printer": ["@webassemblyjs/wast-printer@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw=="],
 
+    "@workos-inc/authkit-nextjs": ["@workos-inc/authkit-nextjs@0.14.0", "", { "dependencies": { "@workos-inc/node": "^7.29.0", "iron-session": "^8.0.1", "jose": "^5.2.3", "path-to-regexp": "^6.2.2" }, "peerDependencies": { "next": "^13.5.4 || ^14.0.3 || ^15.0.0", "react": "^18.0 || ^19.0.0", "react-dom": "^18.0 || ^19.0.0" } }, "sha512-6hBBw4HPq00aP4jO0DHuF4gO6U+MLCeJLJ+eJSRqNSw0rhojJLbmNNyG55XH2OgNhKk2qomSlqBqurFnFq7lZA=="],
+
+    "@workos-inc/node": ["@workos-inc/node@7.74.2", "", { "dependencies": { "iron-session": "~6.3.1", "jose": "~5.6.3", "leb": "^1.0.0", "qs": "6.14.0" } }, "sha512-uXLEUqNo7AkM8oBzVPY29uyEpU0r8Jt2SppL2KzwFWB3f0hLLzp51wrIQ4PNJgd9qI5L/+kP62SoZFc8byI+4g=="],
+
     "@xtuc/ieee754": ["@xtuc/ieee754@1.2.0", "", {}, "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA=="],
 
     "@xtuc/long": ["@xtuc/long@4.2.2", "", {}, "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="],
@@ -1172,6 +1210,8 @@
 
     "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="],
 
+    "asn1js": ["asn1js@3.0.6", "", { "dependencies": { "pvtsutils": "^1.3.6", "pvutils": "^1.1.3", "tslib": "^2.8.1" } }, "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA=="],
+
     "ast-types-flow": ["ast-types-flow@0.0.8", "", {}, "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ=="],
 
     "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
@@ -1200,6 +1240,8 @@
 
     "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
 
+    "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
+
     "baseline-browser-mapping": ["baseline-browser-mapping@2.8.14", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-GM9c0cWWR8Ga7//Ves/9KRgTS8nLausCkP3CGiFLrnwA2CDUluXgaQqvrULoR2Ujrd/mz/lkX87F5BHFsNr5sQ=="],
 
     "bignumber.js": ["bignumber.js@9.3.0", "", {}, "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA=="],
@@ -1218,6 +1260,8 @@
 
     "bser": ["bser@2.1.1", "", { "dependencies": { "node-int64": "^0.4.0" } }, "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ=="],
 
+    "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
+
     "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
 
     "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
@@ -1302,7 +1346,7 @@
 
     "convex": ["convex@1.29.0", "", { "dependencies": { "esbuild": "0.25.4", "prettier": "^3.0.0" }, "peerDependencies": { "@auth0/auth0-react": "^2.0.1", "@clerk/clerk-react": "^4.12.8 || ^5.0.0", "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0" }, "optionalPeers": ["@auth0/auth0-react", "@clerk/clerk-react", "react"], "bin": { "convex": "bin/main.js" } }, "sha512-uoIPXRKIp2eLCkkR9WJ2vc9NtgQtx8Pml59WPUahwbrd5EuW2WLI/cf2E7XrUzOSifdQC3kJZepisk4wJNTJaA=="],
 
-    "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
+    "cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="],
 
     "cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="],
 
@@ -1660,6 +1704,8 @@
 
     "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
 
+    "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
+
     "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
 
     "immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="],
@@ -1688,6 +1734,10 @@
 
     "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
 
+    "iron-session": ["iron-session@8.0.4", "", { "dependencies": { "cookie": "^0.7.2", "iron-webcrypto": "^1.2.1", "uncrypto": "^0.1.3" } }, "sha512-9ivNnaKOd08osD0lJ3i6If23GFS2LsxyMU8Gf/uBUEgm8/8CC1hrrCHFDpMo3IFbpBgwoo/eairRsaD3c5itxA=="],
+
+    "iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="],
+
     "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="],
 
     "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="],
@@ -1870,6 +1920,8 @@
 
     "language-tags": ["language-tags@1.0.9", "", { "dependencies": { "language-subtag-registry": "^0.3.20" } }, "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA=="],
 
+    "leb": ["leb@1.0.0", "", {}, "sha512-Y3c3QZfvKWHX60BVOQPhLCvVGmDYWyJEiINE3drOog6KCyN2AOwvuQQzlS3uJg1J85kzpILXIUwRXULWavir+w=="],
+
     "leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="],
 
     "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
@@ -2120,7 +2172,7 @@
 
     "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
 
-    "path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="],
+    "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="],
 
     "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
 
@@ -2178,6 +2230,10 @@
 
     "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="],
 
+    "pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="],
+
+    "pvutils": ["pvutils@1.1.5", "", {}, "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA=="],
+
     "qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="],
 
     "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
@@ -2464,6 +2520,8 @@
 
     "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
 
+    "uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
+
     "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
 
     "unicode-emoji-modifier-base": ["unicode-emoji-modifier-base@1.0.0", "", {}, "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g=="],
@@ -2528,6 +2586,8 @@
 
     "web-vitals": ["web-vitals@5.1.0", "", {}, "sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg=="],
 
+    "webcrypto-core": ["webcrypto-core@1.8.1", "", { "dependencies": { "@peculiar/asn1-schema": "^2.3.13", "@peculiar/json-schema": "^1.1.12", "asn1js": "^3.0.5", "pvtsutils": "^1.3.5", "tslib": "^2.7.0" } }, "sha512-P+x1MvlNCXlKbLSOY4cYrdreqPG5hbzkmawbcXLKN/mf6DZW0SdNNkZ+sjwsqVkI4A4Ko2sPZmkZtCKY58w83A=="],
+
     "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
 
     "webpack": ["webpack@5.102.1", "", { "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.15.0", "acorn-import-phases": "^1.0.3", "browserslist": "^4.26.3", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.17.3", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", "terser-webpack-plugin": "^5.3.11", "watchpack": "^2.4.4", "webpack-sources": "^3.3.3" }, "bin": { "webpack": "bin/webpack.js" } }, "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ=="],
@@ -2952,6 +3012,14 @@
 
     "@typescript-eslint/typescript-estree/semver": ["semver@7.7.2", "", { "bin": "bin/semver.js" }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
 
+    "@workos-inc/authkit-nextjs/jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="],
+
+    "@workos-inc/node/iron-session": ["iron-session@6.3.1", "", { "dependencies": { "@peculiar/webcrypto": "^1.4.0", "@types/cookie": "^0.5.1", "@types/express": "^4.17.13", "@types/koa": "^2.13.5", "@types/node": "^17.0.41", "cookie": "^0.5.0", "iron-webcrypto": "^0.2.5" }, "peerDependencies": { "express": ">=4", "koa": ">=2", "next": ">=10" }, "optionalPeers": ["express", "koa", "next"] }, "sha512-3UJ7y2vk/WomAtEySmPgM6qtYF1cZ3tXuWX5GsVX4PJXAcs5y/sV9HuSfpjKS6HkTL/OhZcTDWJNLZ7w+Erx3A=="],
+
+    "@workos-inc/node/jose": ["jose@5.6.3", "", {}, "sha512-1Jh//hEEwMhNYPDDLwXHa2ePWgWiFNNUadVmguAAw2IJ6sj9mNxV5tGXJNqlMkJAybF6Lgw1mISDxTePP/187g=="],
+
+    "@workos-inc/node/qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
+
     "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
 
     "ajv-formats/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
@@ -2994,10 +3062,10 @@
 
     "execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
 
-    "express/cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="],
-
     "express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
 
+    "express/path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="],
+
     "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
 
     "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
@@ -3020,6 +3088,8 @@
 
     "inngest/zod": ["zod@4.1.12", "", {}, "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ=="],
 
+    "iron-session/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
+
     "is-bun-module/semver": ["semver@7.7.2", "", { "bin": "bin/semver.js" }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
 
     "istanbul-lib-instrument/semver": ["semver@7.7.2", "", { "bin": "bin/semver.js" }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
@@ -3198,8 +3268,6 @@
 
     "@modelcontextprotocol/sdk/express/content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="],
 
-    "@modelcontextprotocol/sdk/express/cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="],
-
     "@modelcontextprotocol/sdk/express/cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
 
     "@modelcontextprotocol/sdk/express/finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="],
@@ -3436,6 +3504,12 @@
 
     "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
 
+    "@workos-inc/node/iron-session/@types/node": ["@types/node@17.0.45", "", {}, "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw=="],
+
+    "@workos-inc/node/iron-session/cookie": ["cookie@0.5.0", "", {}, "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw=="],
+
+    "@workos-inc/node/iron-session/iron-webcrypto": ["iron-webcrypto@0.2.8", "", { "dependencies": { "buffer": "^6" } }, "sha512-YPdCvjFMOBjXaYuDj5tiHst5CEk6Xw84Jo8Y2+jzhMceclAnb3+vNPP/CTtb5fO2ZEuXEaO4N+w62Vfko757KA=="],
+
     "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
 
     "ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],

File: convex/auth.config.ts
Changes:
@@ -1,8 +1,25 @@
+const workosApiUrl = process.env.WORKOS_API_URL || "https://api.workos.com";
+const workosClientId = process.env.WORKOS_CLIENT_ID;
+
+if (!workosClientId) {
+  throw new Error("WORKOS_CLIENT_ID is not set in the environment");
+}
+
 export default {
   providers: [
     {
-      domain: process.env.CLERK_FRONTEND_API_URL,
-      applicationID: "convex",
+      type: "customJwt",
+      issuer: `${workosApiUrl}/sso`,
+      jwks: `${workosApiUrl}/sso/jwks`,
+      algorithm: "RS256",
+      applicationID: workosClientId,
+    },
+    {
+      type: "customJwt",
+      issuer: `${workosApiUrl}/user_management`,
+      jwks: `${workosApiUrl}/user_management/jwks`,
+      algorithm: "RS256",
+      applicationID: workosClientId,
     },
   ],
 };

File: explanations/WORKOS_MIGRATION_GUIDE.md
Changes:
@@ -0,0 +1,271 @@
+# WorkOS Migration Guide
+
+This guide helps you migrate from Clerk to WorkOS AuthKit authentication.
+
+## Overview
+
+We've migrated from Clerk to WorkOS AuthKit for authentication. WorkOS provides enterprise-ready authentication with better pricing (free up to 1M users) and seamless integration with Convex.
+
+## What Changed
+
+### 1. Authentication Provider
+- **Before**: Clerk (`@clerk/nextjs`)
+- **After**: WorkOS AuthKit (`@workos-inc/authkit-nextjs`)
+
+### 2. Environment Variables
+
+**Remove these Clerk variables:**
+```bash
+NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY
+CLERK_SECRET_KEY
+CLERK_JWT_ISSUER_DOMAIN
+CLERK_WEBHOOK_SECRET
+NEXT_PUBLIC_CLERK_SIGN_IN_URL
+NEXT_PUBLIC_CLERK_SIGN_UP_URL
+NEXT_PUBLIC_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL
+NEXT_PUBLIC_CLERK_SIGN_UP_FALLBACK_REDIRECT_URL
+```
+
+**Add these WorkOS variables:**
+```bash
+WORKOS_API_KEY=sk_test_xxx                           # From WorkOS Dashboard → API Keys
+WORKOS_CLIENT_ID=client_xxx                          # From WorkOS Dashboard → Configuration
+WORKOS_REDIRECT_URI=http://localhost:3000/callback   # Auth callback URL
+WORKOS_WEBHOOK_SECRET=wh_secret_xxx                  # From WorkOS Dashboard → Webhooks
+WORKOS_API_URL=https://api.workos.com                # WorkOS API endpoint
+```
+
+### 3. Files Changed
+
+**Modified:**
+- `src/middleware.ts` - Uses WorkOS middleware
+- `src/components/convex-provider.tsx` - Integrates WorkOS with Convex
+- `src/lib/auth-server.ts` - Uses WorkOS server functions
+- `src/components/user-control.tsx` - Uses WorkOS user hooks
+- `src/app/sign-in/[[...sign-in]]/page.tsx` - Redirects to WorkOS
+- `src/app/sign-up/[[...sign-up]]/page.tsx` - Redirects to WorkOS
+- `src/modules/home/ui/components/navbar.tsx` - Updated auth buttons
+- `convex/auth.config.ts` - Points to WorkOS JWKS endpoint
+
+**Created:**
+- `src/app/callback/route.ts` - OAuth callback handler
+- `src/app/sign-out/route.ts` - Sign out handler
+- `src/app/api/webhooks/workos/route.ts` - WorkOS webhook handler
+
+**Deleted:**
+- `src/lib/clerk-config.ts` - Clerk configuration (no longer needed)
+- `src/app/api/webhooks/clerk/route.ts` - Clerk webhook handler
+
+## Setup Instructions
+
+### Step 1: Create WorkOS Account
+
+1. Go to [WorkOS Dashboard](https://dashboard.workos.com)
+2. Sign up for a free account
+3. Create a new environment (Development)
+
+### Step 2: Configure AuthKit
+
+1. In WorkOS Dashboard, go to **AuthKit** → **Configuration**
+2. Set up your redirect URI:
+   - Development: `http://localhost:3000/callback`
+   - Production: `https://your-domain.com/callback`
+3. Copy your **Client ID**
+
+### Step 3: Get API Keys
+
+1. Go to **API Keys** in WorkOS Dashboard
+2. Copy your **API Key** (starts with `sk_`)
+3. Store securely in your `.env.local` file
+
+### Step 4: Set Up Webhooks
+
+1. Go to **Webhooks** in WorkOS Dashboard
+2. Create a new webhook endpoint:
+   - Development: Use ngrok or similar tunneling service
+   - Production: `https://your-domain.com/api/webhooks/workos`
+3. Subscribe to these events:
+   - `user.created`
+   - `user.updated`
+   - `user.deleted`
+4. Copy the **Webhook Secret**
+
+### Step 5: Update Environment Variables
+
+Create or update your `.env.local` file:
+
+```bash
+# WorkOS Authentication
+WORKOS_API_KEY=sk_test_YOUR_API_KEY
+WORKOS_CLIENT_ID=client_YOUR_CLIENT_ID
+WORKOS_REDIRECT_URI=http://localhost:3000/callback
+WORKOS_WEBHOOK_SECRET=wh_secret_YOUR_WEBHOOK_SECRET
+WORKOS_API_URL=https://api.workos.com
+```
+
+### Step 6: Update Convex Deployment
+
+If you're using Convex, update the environment variables:
+
+```bash
+npx convex env set WORKOS_API_URL https://api.workos.com
+```
+
+Or set it in your Convex Dashboard → Settings → Environment Variables.
+
+### Step 7: Install Dependencies
+
+```bash
+bun install
+```
+
+This will install `@workos-inc/authkit-nextjs` and remove `@clerk/nextjs`.
+
+### Step 8: Test Authentication Flow
+
+1. Start your development server:
+   ```bash
+   bun run dev
+   ```
+
+2. Start Convex:
+   ```bash
+   bun run convex:dev
+   ```
+
+3. Navigate to `http://localhost:3000`
+
+4. Click "Sign in" - you should be redirected to WorkOS AuthKit
+
+5. Create a test account and verify:
+   - You're redirected back to your app
+   - User data appears in user control dropdown
+   - You can access protected routes
+
+## User Data Mapping
+
+WorkOS user object differs from Clerk:
+
+| Clerk | WorkOS |
+|-------|--------|
+| `user.fullName` | `[user.firstName, user.lastName].join(' ')` |
+| `user.primaryEmailAddress.emailAddress` | `user.email` |
+| `user.imageUrl` | `user.profilePictureUrl` |
+| `user.id` | `user.id` |
+
+## Migration Considerations
+
+### User IDs
+- Existing users have Clerk-formatted user IDs in your database
+- WorkOS generates different user ID formats
+- You may need to:
+  1. Add a migration script to map old Clerk IDs to new WorkOS IDs
+  2. Maintain a mapping table during transition period
+  3. Ask existing users to re-authenticate
+
+### Social Logins
+- Reconfigure OAuth apps (Google, GitHub, etc.) in WorkOS Dashboard
+- Update redirect URIs in each OAuth provider to point to WorkOS
+
+### Custom Claims
+- Clerk: Used `publicMetadata` for custom claims (e.g., `plan: "pro"`)
+- WorkOS: Uses organization metadata for custom claims
+- Current implementation: Plan is stored in Convex `subscriptions` table
+
+### Session Management
+- WorkOS sessions work differently from Clerk
+- Session tokens are managed automatically by WorkOS SDK
+- No need to manually refresh tokens
+
+## Troubleshooting
+
+### Issue: "Missing WORKOS_API_KEY"
+**Solution**: Ensure all WorkOS environment variables are set in `.env.local`
+
+### Issue: Redirect loop after sign-in
+**Solution**: Verify `WORKOS_REDIRECT_URI` matches exactly what's configured in WorkOS Dashboard
+
+### Issue: "Invalid session"
+**Solution**: 
+1. Clear browser cookies
+2. Verify `WORKOS_API_URL` is set correctly
+3. Check Convex `auth.config.ts` points to WorkOS
+
+### Issue: Webhook not receiving events
+**Solution**:
+1. For local development, use ngrok: `ngrok http 3000`
+2. Update webhook URL in WorkOS Dashboard
+3. Verify `WORKOS_WEBHOOK_SECRET` matches
+
+### Issue: User data not showing
+**Solution**: 
+1. Check browser console for errors
+2. Verify `useUser()` hook returns data
+3. Ensure session is valid
+
+## Production Deployment
+
+### Vercel Deployment
+
+1. Add all WorkOS environment variables to Vercel:
+   ```bash
+   vercel env add WORKOS_API_KEY
+   vercel env add WORKOS_CLIENT_ID
+   vercel env add WORKOS_REDIRECT_URI
+   vercel env add WORKOS_WEBHOOK_SECRET
+   vercel env add WORKOS_API_URL
+   ```
+
+2. Update `WORKOS_REDIRECT_URI` to production URL:
+   ```
+   https://your-domain.com/callback
+   ```
+
+3. Update WorkOS webhook endpoint to production:
+   ```
+   https://your-domain.com/api/webhooks/workos
+   ```
+
+4. Deploy:
+   ```bash
+   vercel --prod
+   ```
+
+### Convex Production
+
+Update Convex production environment:
+
+```bash
+npx convex deploy
+npx convex env set WORKOS_API_URL https://api.workos.com --prod
+```
+
+## Benefits of WorkOS
+
+1. **Cost**: Free up to 1M users (vs Clerk's pricing)
+2. **Enterprise Ready**: Built-in support for SSO, SCIM, RBAC
+3. **Convex Integration**: Official integration with Convex
+4. **Developer Experience**: Simple API, excellent docs
+5. **Flexibility**: Easy to customize authentication flows
+
+## Support
+
+- [WorkOS Documentation](https://workos.com/docs)
+- [WorkOS Next.js Guide](https://workos.com/docs/authkit/nextjs)
+- [Convex + WorkOS Integration](https://docs.convex.dev/auth/authkit)
+
+## Migration Checklist
+
+- [ ] Create WorkOS account
+- [ ] Configure AuthKit in WorkOS Dashboard
+- [ ] Get API keys and webhook secret
+- [ ] Update `.env.local` with WorkOS variables
+- [ ] Run `bun install`
+- [ ] Test sign-in/sign-up flow locally
+- [ ] Verify user data displays correctly
+- [ ] Test protected routes
+- [ ] Set up webhooks for production
+- [ ] Update Vercel environment variables
+- [ ] Deploy to production
+- [ ] Test production authentication flow
+- [ ] Monitor for errors in Sentry/logs

File: package.json
Changes:
@@ -12,7 +12,7 @@
     "convex:deploy": "bunx convex deploy"
   },
   "dependencies": {
-    "@clerk/nextjs": "^6.35.5",
+    "@workos-inc/authkit-nextjs": "^0.14.0",
     "@databuddy/sdk": "^2.2.1",
     "@e2b/code-interpreter": "^1.5.1",
     "@hookform/resolvers": "^3.3.4",

File: src/app/api/webhooks/clerk/route.ts
Changes:
@@ -1,94 +0,0 @@
-import { Webhook } from "svix";
-import { headers } from "next/headers";
-import { WebhookEvent } from "@clerk/nextjs/server";
-import { ConvexHttpClient } from "convex/browser";
-import { api } from "@/convex/_generated/api";
-
-export async function POST(req: Request) {
-    // You can find this in the Clerk Dashboard -> Webhooks -> choose the webhook
-    const WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET;
-
-    if (!WEBHOOK_SECRET) {
-        throw new Error(
-            "Please add CLERK_WEBHOOK_SECRET from Clerk Dashboard to .env or .env.local"
-        );
-    }
-
-    // Get the headers
-    const headerPayload = await headers();
-    const svix_id = headerPayload.get("svix-id");
-    const svix_timestamp = headerPayload.get("svix-timestamp");
-    const svix_signature = headerPayload.get("svix-signature");
-
-    // If there are no headers, error out
-    if (!svix_id || !svix_timestamp || !svix_signature) {
-        return new Response("Error occured -- no svix headers", {
-            status: 400,
-        });
-    }
-
-    // Get the body
-    const payload = await req.json();
-    const body = JSON.stringify(payload);
-
-    // Create a new Svix instance with your secret.
-    const wh = new Webhook(WEBHOOK_SECRET);
-
-    let evt: WebhookEvent;
-
-    // Verify the payload with the headers
-    try {
-        evt = wh.verify(body, {
-            "svix-id": svix_id,
-            "svix-timestamp": svix_timestamp,
-            "svix-signature": svix_signature,
-        }) as WebhookEvent;
-    } catch (err) {
-        console.error("Error verifying webhook:", err);
-        return new Response("Error occured", {
-            status: 400,
-        });
-    }
-
-    // Get the event type
-    const eventType = evt.type;
-
-    // Initialize Convex client
-    const convex = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
-
-    if (eventType === "user.created") {
-        const { id, email_addresses, first_name, last_name, image_url } = evt.data;
-
-        const email = email_addresses[0]?.email_address;
-        const name = `${first_name || ""} ${last_name || ""}`.trim();
-
-        console.log(`User created: ${id} (${email})`);
-
-        // Initialize usage for the new user
-        // This ensures they have free tier credits immediately
-        try {
-            await convex.mutation(api.usage.checkAndConsumeCreditForUser, {
-                userId: id,
-            });
-            // We actually just want to initialize, not consume. 
-            // But checkAndConsumeCreditForUser consumes 1 credit.
-            // Let's use a specific initialization mutation if possible, or just accept the 1 credit cost (unideal).
-            // Looking at usage.ts, there isn't a pure "init" mutation exposed.
-            // However, `getUsageForUser` calls `getUsageInternal` which does NOT create the record if missing (it just returns default values).
-            // `checkAndConsumeCreditInternal` creates it if missing.
-
-            // Let's rely on lazy initialization for now as per the existing design in `usage.ts`.
-            // The `usage.ts` logic says: "If no usage record or expired, create/reset with max points" inside `checkAndConsumeCredit`.
-            // So we don't strictly NEED to do anything here for usage.
-            // But we might want to store the user in a `users` table if we had one. 
-            // Since we don't have a `users` table (as per schema review), we just log it.
-
-            console.log(`User ${id} created. Usage will be initialized lazily on first generation.`);
-
-        } catch (error) {
-            console.error("Error initializing user data:", error);
-        }
-    }
-
-    return new Response("", { status: 200 });
-}

File: src/app/api/webhooks/workos/route.ts
Changes:
@@ -0,0 +1,93 @@
+import { WorkOS } from "@workos-inc/node";
+import { headers } from "next/headers";
+
+let workos: WorkOS | null = null;
+
+const getWorkOSClient = () => {
+  if (!workos) {
+    const WORKOS_API_KEY = process.env.WORKOS_API_KEY;
+
+    if (!WORKOS_API_KEY) {
+      throw new Error(
+        "Please add WORKOS_API_KEY from WorkOS Dashboard to .env or .env.local"
+      );
+    }
+
+    workos = new WorkOS(WORKOS_API_KEY);
+  }
+
+  return workos;
+};
+
+export async function POST(req: Request) {
+  // Get WorkOS webhook secret
+  const WEBHOOK_SECRET = process.env.WORKOS_WEBHOOK_SECRET;
+
+  if (!WEBHOOK_SECRET) {
+    throw new Error(
+      "Please add WORKOS_WEBHOOK_SECRET from WorkOS Dashboard to .env or .env.local"
+    );
+  }
+
+  // Get the headers
+  const headerPayload = headers();
+  const signature = headerPayload.get("workos-signature");
+
+  if (!signature) {
+    return new Response("Error occurred -- no WorkOS signature header", {
+      status: 400,
+    });
+  }
+
+  const rawBody = await req.text();
+
+  let event;
+
+  try {
+    const payload = JSON.parse(rawBody);
+
+    event = await getWorkOSClient().webhooks.constructEvent({
+      payload,
+      sigHeader: signature,
+      secret: WEBHOOK_SECRET,
+    });
+  } catch (error) {
+    console.error("Error verifying WorkOS webhook:", error);
+
+    return new Response("Invalid signature", { status: 400 });
+  }
+
+  const { event: eventType, data } = event;
+
+  if (eventType === "user.created") {
+    const { id, email, firstName, lastName, profilePictureUrl } = data;
+
+    console.log(`User created: ${id} (${email})`, {
+      firstName,
+      lastName,
+      profilePictureUrl,
+    });
+
+    // Initialize usage for the new user
+    try {
+      // The usage system will initialize lazily on first generation
+      console.log(`User ${id} created. Usage will be initialized lazily on first generation.`);
+    } catch (error) {
+      console.error("Error initializing user data:", error);
+    }
+  }
+
+  if (eventType === "user.updated") {
+    const { id, email } = data;
+    console.log(`User updated: ${id} (${email})`);
+    // Handle user updates if needed
+  }
+
+  if (eventType === "user.deleted") {
+    const { id } = data;
+    console.log(`User deleted: ${id}`);
+    // Handle user deletion if needed (cleanup projects, messages, etc.)
+  }
+
+  return new Response("", { status: 200 });
+}

File: src/app/callback/route.ts
Changes:
@@ -0,0 +1,3 @@
+import { handleAuth } from "@workos-inc/authkit-nextjs";
+
+export const GET = handleAuth();

File: src/app/sign-in/[[...sign-in]]/page.tsx
Changes:
@@ -1,9 +1,7 @@
-import { SignIn } from "@clerk/nextjs";
+import { getSignInUrl } from "@workos-inc/authkit-nextjs";
+import { redirect } from "next/navigation";
 
-export default function Page() {
-    return (
-        <div className="flex h-screen w-full items-center justify-center">
-            <SignIn />
-        </div>
-    );
+export default async function SignInPage() {
+  const signInUrl = await getSignInUrl();
+  redirect(signInUrl);
 }

File: src/app/sign-out/route.ts
Changes:
@@ -0,0 +1,7 @@
+import { NextResponse } from "next/server";
+import { signOut } from "@workos-inc/authkit-nextjs";
+
+export async function GET(request: Request) {
+  await signOut();
+  return NextResponse.redirect(new URL("/", request.url));
+}

File: src/app/sign-up/[[...sign-up]]/page.tsx
Changes:
@@ -1,9 +1,7 @@
-import { SignUp } from "@clerk/nextjs";
+import { getSignUpUrl } from "@workos-inc/authkit-nextjs";
+import { redirect } from "next/navigation";
 
-export default function Page() {
-    return (
-        <div className="flex h-screen w-full items-center justify-center">
-            <SignUp />
-        </div>
-    );
+export default async function SignUpPage() {
+  const signUpUrl = await getSignUpUrl();
+  redirect(signUpUrl);
 }

File: src/components/convex-provider.tsx
Changes:
@@ -1,32 +1,41 @@
 "use client";
 
-import { ReactNode } from "react";
+import { ReactNode, useMemo } from "react";
 import { ConvexReactClient } from "convex/react";
-import { ClerkProvider, useAuth } from "@clerk/nextjs";
-import { ConvexProviderWithClerk } from "convex/react-clerk";
+import { ConvexProviderWithAuth } from "convex/react";
+import { useAccessToken } from "@workos-inc/authkit-nextjs/components";
 
 if (!process.env.NEXT_PUBLIC_CONVEX_URL) {
   throw new Error("Missing NEXT_PUBLIC_CONVEX_URL in your .env file");
 }
 
-if (!process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY) {
-  throw new Error("Missing NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY in your .env file");
-}
-
 const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL);
 
 export function ConvexClientProvider({ children }: { children: ReactNode }) {
   return (
-    <ClerkProvider 
-      publishableKey={process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY}
-      signInUrl="/sign-in"
-      signUpUrl="/sign-up"
-      signInFallbackRedirectUrl="/"
-      signUpFallbackRedirectUrl="/"
-    >
-      <ConvexProviderWithClerk client={convex} useAuth={useAuth}>
-        {children}
-      </ConvexProviderWithClerk>
-    </ClerkProvider>
+    <ConvexProviderWithAuth client={convex} useAuth={useWorkOSAuth}>
+      {children}
+    </ConvexProviderWithAuth>
+  );
+}
+
+function useWorkOSAuth() {
+  const { accessToken, loading, refresh } = useAccessToken();
+
+  return useMemo(
+    () => ({
+      isLoading: loading,
+      isAuthenticated: !!accessToken,
+      fetchAccessToken: async ({
+        forceRefreshToken,
+      }: {
+        forceRefreshToken: boolean;
+      }) => {
+        if (!accessToken && !forceRefreshToken) return null;
+        if (forceRefreshToken) await refresh();
+        return accessToken ?? null;
+      },
+    }),
+    [accessToken, loading]
   );
 }

File: src/components/user-control.tsx
Changes:
@@ -1,6 +1,6 @@
 "use client";
 
-import { useUser, useClerk } from "@clerk/nextjs";
+import { useUser } from "@workos-inc/authkit-nextjs";
 import { useRouter } from "next/navigation";
 import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
 import {
@@ -20,24 +20,23 @@ interface Props {
 export const UserControl = ({ showName }: Props) => {
   const router = useRouter();
   const { user } = useUser();
-  const { signOut } = useClerk();
 
   if (!user) return null;
 
   const handleSignOut = async () => {
-    await signOut();
-    router.push("/");
+    // WorkOS sign out - redirect to sign out endpoint
+    window.location.href = "/sign-out";
   };
 
-  const displayName = user.fullName || user.primaryEmailAddress?.emailAddress || "User";
+  const displayName = [user.firstName, user.lastName].filter(Boolean).join(" ") || user.email || "User";
   const initials = displayName
     ?.split(" ")
     .map((n) => n[0])
     .join("")
     .toUpperCase()
     .slice(0, 2) || "U";
 
-  const avatarSrc = user.imageUrl || undefined;
+  const avatarSrc = user.profilePictureUrl || undefined;
 
   return (
     <DropdownMenu>
@@ -57,7 +56,7 @@ export const UserControl = ({ showName }: Props) => {
           <div className="flex flex-col space-y-1">
             <p className="text-sm font-medium leading-none">{displayName}</p>
             <p className="text-xs leading-none text-muted-foreground">
-              {user.primaryEmailAddress?.emailAddress}
+              {user.email}
             </p>
           </div>
         </DropdownMenuLabel>

File: src/lib/auth-server.ts
Changes:
@@ -1,19 +1,18 @@
 import { ConvexHttpClient } from "convex/browser";
-import { auth, currentUser } from "@clerk/nextjs/server";
+import { getUser as getWorkOSUser, getAccessToken, getSignInUrl, getSignUpUrl, signOut } from "@workos-inc/authkit-nextjs";
 
 /**
- * Get the authenticated user from Clerk
+ * Get the authenticated user from WorkOS
  */
 export async function getUser() {
-  return await currentUser();
+  return await getWorkOSUser();
 }
 
 /**
  * Get the authentication token for Convex
  */
 export async function getToken() {
-  const { getToken } = await auth();
-  return await getToken({ template: "convex" });
+  return await getAccessToken();
 }
 
 /**
@@ -28,7 +27,7 @@ export async function getAuthHeaders() {
 }
 
 /**
- * Create a Convex HTTP client with Clerk authentication
+ * Create a Convex HTTP client with WorkOS authentication
  * Use this in API routes that need to call Convex
  */
 export async function getConvexClientWithAuth() {
@@ -47,3 +46,8 @@ export async function getConvexClientWithAuth() {
 
   return httpClient;
 }
+
+/**
+ * Get WorkOS sign in URL
+ */
+export { getSignInUrl, getSignUpUrl, signOut };

File: src/lib/clerk-config.ts
Changes:
@@ -1,117 +0,0 @@
-/**
- * Centralizes Clerk-related environment handling with sensible fallbacks.
- * - Accepts either publishableKey or frontendApi (host only, no protocol).
- * - Normalizes optional proxy/sign-in URLs and trims stray slashes/protocols.
- * - Validates proxy URLs to prevent CORS/404 errors from invalid domains.
- */
-export function getClerkInstanceConfig() {
-  const publishableKey =
-    process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY ||
-    process.env.CLERK_PUBLISHABLE_KEY ||
-    process.env.VITE_CLERK_PUBLISHABLE_KEY ||
-    undefined;
-
-  const rawFrontendApi =
-    process.env.NEXT_PUBLIC_CLERK_FRONTEND_API ||
-    process.env.CLERK_FRONTEND_API_URL ||
-    process.env.NEXT_CLERK_FRONTEND_API_URL ||
-    process.env.CLERK_JWT_ISSUER_DOMAIN;
-  
-  const frontendApi = validateClerkDomain(rawFrontendApi);
-
-  if (!publishableKey && !frontendApi) {
-    throw new Error(
-      "Missing Clerk configuration. Set NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY or NEXT_PUBLIC_CLERK_FRONTEND_API",
-    );
-  }
-
-  return {
-    publishableKey,
-    frontendApi,
-    proxyUrl: validateProxyUrl(process.env.NEXT_PUBLIC_CLERK_PROXY_URL),
-    signInUrl:
-      normalizePath(process.env.NEXT_PUBLIC_CLERK_SIGN_IN_URL) || "/sign-in",
-    signUpUrl:
-      normalizePath(process.env.NEXT_PUBLIC_CLERK_SIGN_UP_URL) || "/sign-up",
-    signInFallbackRedirectUrl:
-      normalizePath(
-        process.env.NEXT_PUBLIC_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL,
-      ) || "/",
-    signUpFallbackRedirectUrl:
-      normalizePath(
-        process.env.NEXT_PUBLIC_CLERK_SIGN_UP_FALLBACK_REDIRECT_URL,
-      ) || "/",
-    appUrl: normalizeUrl(process.env.NEXT_PUBLIC_APP_URL),
-  };
-}
-
-function normalizeHost(value?: string | null) {
-  if (!value) return undefined;
-  return value.replace(/^https?:\/\//, "").replace(/\/$/, "");
-}
-
-function normalizeUrl(value?: string | null) {
-  if (!value) return undefined;
-  return value.replace(/\/$/, "");
-}
-
-function normalizePath(value?: string | null) {
-  if (!value) return undefined;
-  return value.startsWith("/") ? value : `/${value}`;
-}
-
-const VALID_CLERK_DOMAINS = [
-  ".clerk.accounts.dev",
-  ".clerk.dev",
-  ".clerkstage.dev",
-  ".lclclerk.com",
-  "clerk.zapdev.link",
-];
-
-function isValidClerkDomain(hostname: string): boolean {
-  const lower = hostname.toLowerCase();
-  return VALID_CLERK_DOMAINS.some(domain => 
-    domain.startsWith(".") ? lower.endsWith(domain) : lower === domain
-  );
-}
-
-function validateClerkDomain(value?: string | null): string | undefined {
-  if (!value) return undefined;
-
-  const normalized = normalizeHost(value);
-  if (!normalized) return undefined;
-
-  if (isValidClerkDomain(normalized)) {
-    return normalized;
-  }
-
-  console.warn(
-    `[Clerk Config] Invalid Clerk domain "${normalized}" - must be a valid Clerk domain (e.g., *.clerk.accounts.dev). Ignoring to prevent CORS/404 errors.`
-  );
-  return undefined;
-}
-
-function validateProxyUrl(value?: string | null): string | undefined {
-  if (!value) return undefined;
-  
-  const normalized = normalizeUrl(value);
-  if (!normalized) return undefined;
-  
-  try {
-    const url = new URL(normalized);
-    
-    if (!isValidClerkDomain(url.hostname)) {
-      console.warn(
-        `[Clerk Config] Invalid proxy URL "${normalized}" - must be a Clerk domain. Ignoring to prevent CORS/404 errors.`
-      );
-      return undefined;
-    }
-    
-    return normalized;
-  } catch {
-    console.warn(
-      `[Clerk Config] Invalid proxy URL format "${value}". Ignoring.`
-    );
-    return undefined;
-  }
-}

File: src/middleware.ts
Changes:
@@ -1,20 +1,20 @@
-import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
+import { authkitMiddleware } from "@workos-inc/authkit-nextjs";
 
-const isPublicRoute = createRouteMatcher([
-  "/",
-  "/sign-in(.*)",
-  "/sign-up(.*)",
-  "/api/webhooks(.*)",
-  "/api/inngest(.*)",
-  "/api/uploadthing(.*)",
-  "/api/vitals",
-  "/api/rss",
-]);
-
-export default clerkMiddleware(async (auth, request) => {
-  if (!isPublicRoute(request)) {
-    await auth.protect();
-  }
+export default authkitMiddleware({
+  middlewareAuth: {
+    enabled: true,
+    unauthenticatedPaths: [
+      "/",
+      "/sign-in",
+      "/sign-up",
+      "/pricing",
+      "/api/webhooks(.*)",
+      "/api/inngest(.*)",
+      "/api/uploadthing(.*)",
+      "/api/vitals",
+      "/api/rss",
+    ],
+  },
 });
 
 export const config = {

File: src/modules/home/ui/components/navbar.tsx
Changes:
@@ -6,7 +6,7 @@ import { cn } from "@/lib/utils";
 import { useScroll } from "@/hooks/use-scroll";
 import { Button } from "@/components/ui/button";
 import { UserControl } from "@/components/user-control";
-import { useUser, SignInButton, SignUpButton } from "@clerk/nextjs";
+import { useAuth } from "@workos-inc/authkit-nextjs/components";
 import {
   NavigationMenu,
   NavigationMenuItem,
@@ -25,7 +25,13 @@ import { CalendarCheckIcon, MailIcon } from "lucide-react";
 
 export const Navbar = () => {
   const isScrolled = useScroll();
-  const { user } = useUser();
+  const { user, loading, organizationId, refreshAuth } = useAuth();
+
+  // Keep organization context and refresh handler available when needed.
+  void organizationId;
+  void refreshAuth;
+
+  if (loading) return null;
 
   return (
     <>
@@ -86,16 +92,12 @@ export const Navbar = () => {
             </DropdownMenu>
             {!user ? (
               <div className="flex gap-2">
-                <SignUpButton mode="modal">
-                  <Button variant="outline" size="sm">
-                    Sign up
-                  </Button>
-                </SignUpButton>
-                <SignInButton mode="modal">
-                  <Button size="sm">
-                    Sign in
-                  </Button>
-                </SignInButton>
+                <Button variant="outline" size="sm" asChild>
+                  <Link href="/sign-up">Sign up</Link>
+                </Button>
+                <Button size="sm" asChild>
+                  <Link href="/sign-in">Sign in</Link>
+                </Button>
               </div>
             ) : (
               <UserControl showName />

@codecapyai
Copy link

codecapyai bot commented Nov 28, 2025

🚀 Scrapybara Ubuntu instance started!

Interactive stream

⚠️ Error fetching GitHub variables, continuing setup:

status_code: 502, body: {'detail': "Error communicating with container: Client error '400 Bad Request' for url 'http://54.176.2.220:30038/env'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400"}

@codecapyai
Copy link

codecapyai bot commented Nov 28, 2025

🔧 Setting up test environment...

Agent Steps

@codecapyai
Copy link

codecapyai bot commented Nov 28, 2025

❌ Something went wrong:

status_code: 502, body: {'detail': {'error': "Unexpected error: Error code: 404 - {'type': 'error', 'error': {'type': 'not_found_error', 'message': 'model: claude-3-5-sonnet-20241022'}, 'request_id': 'req_011CVZWwRVaE3yAbvwvst4DT'}", 'provider': 'anthropic', 'error_type': 'ProviderAPIError'}}

@claude
Copy link

claude bot commented Nov 28, 2025

Code Review: WorkOS AuthKit Migration

This is a well-executed authentication migration from Clerk to WorkOS AuthKit. The implementation is clean and follows best practices. Here's my detailed review:


Strengths

1. Clean Migration Approach

  • Complete removal of Clerk dependencies with no leftover code
  • Proper file organization (deleted old webhook handler, created new ones)
  • Comprehensive documentation in WORKOS_MIGRATION_GUIDE.md

2. Security Implementation

  • ✅ Webhook signature verification implemented correctly (src/app/api/webhooks/workos/route.ts:49-53)
  • ✅ Proper error handling for missing secrets (route.ts:24-30)
  • ✅ Environment variable validation in auth config (convex/auth.config.ts:4-6)
  • ✅ JWT validation via JWKS endpoints configured properly

3. Code Quality

  • Well-structured Convex auth integration with WorkOS tokens
  • Clean separation of concerns in useWorkOSAuth hook
  • Proper memoization to prevent unnecessary re-renders (convex-provider.tsx:25-40)
  • Good error messages with actionable guidance

4. Documentation

  • Excellent migration guide with step-by-step instructions
  • Updated all project documentation (CLAUDE.md, AGENTS.md, README.md)
  • Clear environment variable documentation

⚠️ Issues & Concerns

1. Critical: Missing Dependency in useMemo (src/components/convex-provider.tsx:39)

Severity: High

return useMemo(
  () => ({
    // ... uses refresh() here
  }),
  [accessToken, loading]  // ❌ Missing 'refresh' in dependency array
);

Issue: The refresh function is called on line 35 but not included in the dependency array. This violates React Hook rules and could cause stale closures.

Fix:

[accessToken, loading, refresh]

2. Security: Potential Timing Attack (src/app/api/webhooks/workos/route.ts:56)

Severity: Medium

The error handling reveals whether signature verification failed vs. other errors:

catch (error) {
  console.error("Error verifying WorkOS webhook:", error);
  return new Response("Invalid signature", { status: 400 });
}

Recommendation: Use a generic error message for all verification failures to prevent information leakage.

3. Code Smell: Dead Code in Webhook Handler (route.ts:72-77)

Severity: Low

try {
  console.log(`User ${id} created. Usage will be initialized lazily...`);
} catch (error) {
  console.error("Error initializing user data:", error);
}

Issue: The try-catch block serves no purpose since there's no code that can throw an error. This suggests incomplete implementation or leftover refactoring artifacts.

Recommendation: Remove the try-catch or implement the actual user initialization logic.

4. Missing Error Recovery in Token Refresh (convex-provider.tsx:35)

Severity: Medium

if (forceRefreshToken) await refresh();
return accessToken ?? null;

Issue: If refresh() fails/throws, the error isn't caught. The user would see an unhandled promise rejection.

Recommendation:

if (forceRefreshToken) {
  try {
    await refresh();
  } catch (error) {
    console.error('Token refresh failed:', error);
    return null;
  }
}

5. Performance: Singleton Pattern Could Fail (route.ts:4-6)

Severity: Low

let workos: WorkOS | null = null;

const getWorkOSClient = () => {
  if (!workos) {
    workos = new WorkOS(WORKOS_API_KEY);
  }
  return workos;
};

Issue: In serverless environments, this module-level singleton may not provide the expected benefit. Each new instance/cold start resets the variable.

Recommendation: Not a bug, but consider whether the complexity is worth it. A simple new WorkOS() call might be clearer and have similar performance characteristics in serverless.


📊 Testing Concerns

Missing Test Coverage

Severity: High

No test files were added for:

  • WorkOS webhook signature verification
  • Token refresh flow in Convex provider
  • Auth middleware configuration
  • Error handling paths

Recommendation: Add tests for:

  1. Webhook handler with valid/invalid signatures
  2. useWorkOSAuth hook behavior (loading states, token refresh)
  3. Middleware redirect logic
  4. Error scenarios (missing env vars, failed token refresh)

🎯 Specific Recommendations

1. Add Type Safety for Webhook Events

type WorkOSWebhookEvent = 
  | { event: 'user.created'; data: UserCreatedData }
  | { event: 'user.updated'; data: UserUpdatedData }
  | { event: 'user.deleted'; data: UserDeletedData };

// Then use discriminated union for type-safe handling

2. Middleware Configuration Clarity

The regex in middleware.ts:22 is complex. Consider adding a comment explaining what it matches/excludes.

3. Migration Path for Existing Users

The migration guide doesn't address what happens to existing Clerk users. Consider documenting:

  • User data migration strategy
  • Session handling during migration
  • Rollback plan

4. Environment Variable Validation

Consider using a schema validator (like Zod) at app startup to validate all required WorkOS env vars are present, rather than checking at runtime.


📝 Minor Improvements

  1. Line 53 (auth-server.ts): The named exports are redundant since you're already importing and re-exporting them on line 2. Use export { ... } from ... syntax.

  2. Consistent error handling: Some errors throw, others return responses. Standardize the approach.

  3. Logging strategy: Production logs contain potentially sensitive user info (email, names). Consider a structured logging approach with configurable log levels.


🎉 Overall Assessment

Rating: 7.5/10 - Good work with room for improvement

Pros:

  • Solid implementation of core authentication flow
  • Excellent documentation
  • Clean code structure
  • Proper security fundamentals

Must-Fix Before Merge:

  • Add refresh to useMemo dependency array
  • Add error handling to token refresh

Should-Fix:

  • Add test coverage for critical paths
  • Remove dead code in webhook handler
  • Improve error messages (don't leak implementation details)

Nice-to-Have:

  • Type safety improvements for webhook events
  • Migration guide for existing users
  • Structured logging

Great work on this migration! The code quality is generally high, and the migration appears thorough. Addressing the critical issues and adding tests would make this production-ready. 🚀

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (3)
src/modules/home/ui/components/navbar.tsx (1)

9-9: Same module import issue - verify correct import path for useAuth.

This file has the same CI failure as convex-provider.tsx. The @workos-inc/authkit-nextjs/components subpath may not exist. Check the package exports to find the correct import path.

src/components/convex-provider.tsx (1)

35-39: Add refresh to dependency array to prevent stale closure.

refresh is invoked in fetchAccessToken but omitted from the useMemo dependencies. If refresh changes identity between renders, the memoized callback will reference a stale function.

     }),
-    [accessToken, loading]
+    [accessToken, loading, refresh]
   );
convex/auth.config.ts (1)

10-23: Based on official WorkOS and Convex documentation, I can now verify the review comment's accuracy.

JWKS URL format requires the clientId in the path—current implementation is incorrect.

The review comment's core concern is valid. Official documentation confirms:

  • JWKS endpoint must be: https://api.workos.com/sso/jwks/{clientId} (not just /sso/jwks)
  • First issuer should be: https://api.workos.com/
  • Second issuer should be: https://api.workos.com/user_management/{clientId}
  • Both providers use the same JWKS endpoint

The current code omits the {clientId} from the JWKS path, which will fail token verification.

The suggested diff is correct. Apply it:

     {
       type: "customJwt",
-      issuer: `${workosApiUrl}/sso`,
-      jwks: `${workosApiUrl}/sso/jwks`,
+      issuer: `${workosApiUrl}/`,
+      jwks: `${workosApiUrl}/sso/jwks/${workosClientId}`,
       algorithm: "RS256",
       applicationID: workosClientId,
     },
     {
       type: "customJwt",
-      issuer: `${workosApiUrl}/user_management`,
-      jwks: `${workosApiUrl}/user_management/jwks`,
+      issuer: `${workosApiUrl}/user_management/${workosClientId}`,
+      jwks: `${workosApiUrl}/sso/jwks/${workosClientId}`,
       algorithm: "RS256",
       applicationID: workosClientId,
     },
🧹 Nitpick comments (2)
src/app/api/webhooks/workos/route.ts (1)

71-77: Remove empty try-catch or add actual initialization logic.

This try-catch wraps only a log statement and catches errors that can never occur. Either implement the lazy initialization or remove the dead code.

     // Initialize usage for the new user
-    try {
-      // The usage system will initialize lazily on first generation
-      console.log(`User ${id} created. Usage will be initialized lazily on first generation.`);
-    } catch (error) {
-      console.error("Error initializing user data:", error);
-    }
+    // The usage system will initialize lazily on first generation
+    console.log(`User ${id} created. Usage will be initialized lazily on first generation.`);
src/modules/home/ui/components/navbar.tsx (1)

30-32: Consider using an underscore prefix instead of void statements for unused destructured variables.

The void pattern works but is unconventional. A more common TypeScript/ESLint pattern is prefixing with underscore.

-  const { user, loading, organizationId, refreshAuth } = useAuth();
-
-  // Keep organization context and refresh handler available when needed.
-  void organizationId;
-  void refreshAuth;
+  const { user, loading, organizationId: _organizationId, refreshAuth: _refreshAuth } = useAuth();

Alternatively, if these aren't needed, simply omit them from destructuring.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c13163b and 90fd5a8.

📒 Files selected for processing (5)
  • convex/auth.config.ts (1 hunks)
  • src/app/api/webhooks/workos/route.ts (1 hunks)
  • src/app/sign-out/route.ts (1 hunks)
  • src/components/convex-provider.tsx (1 hunks)
  • src/modules/home/ui/components/navbar.tsx (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/app/sign-out/route.ts (2)
src/app/callback/route.ts (1)
  • GET (3-3)
src/lib/auth-server.ts (1)
  • signOut (53-53)
src/modules/home/ui/components/navbar.tsx (1)
src/components/ui/button.tsx (1)
  • Button (60-60)
🪛 GitHub Actions: CI
src/app/api/webhooks/workos/route.ts

[error] 34-35: Property 'get' does not exist on type 'Promise'.

src/components/convex-provider.tsx

[error] 6-32: Cannot find module '@workos-inc/authkit-nextjs/components' or its corresponding type declarations.

src/modules/home/ui/components/navbar.tsx

[error] 9-25: Cannot find module '@workos-inc/authkit-nextjs/components' or its corresponding type declarations.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Codacy Security Scan
  • GitHub Check: claude-review
🔇 Additional comments (4)
src/app/sign-out/route.ts (1)

4-7: LGTM - Correctly handles sign-out with explicit redirect.

The previous issue has been addressed. The route now properly awaits signOut() and returns an explicit NextResponse.redirect(), which is the correct pattern for a Next.js route handler since signOut() returns Promise<void>.

src/app/api/webhooks/workos/route.ts (1)

46-58: Webhook signature verification is now properly implemented.

The previous security concern has been addressed. The handler now:

  1. Reads the raw body
  2. Parses payload and verifies via webhooks.constructEvent()
  3. Returns 400 on verification failure
src/modules/home/ui/components/navbar.tsx (1)

95-100: Sign-in/sign-up buttons correctly migrated to Link-based navigation.

The Clerk SignInButton/SignUpButton components have been properly replaced with standard Link components pointing to the WorkOS-based routes.

src/components/convex-provider.tsx (1)

6-6: The import path is correct and follows official WorkOS AuthKit documentation—this review comment is incorrect.

The module import @workos-inc/authkit-nextjs/components is the documented and valid export path for useAccessToken in authkit-nextjs v0.14.0. Official WorkOS AuthKit documentation confirms this is the proper subpath. The hook is used correctly throughout the codebase (convex-provider.tsx line 23, navbar.tsx line 9), and no module resolution issue exists.

Likely an incorrect or invalid review comment.

@Jackson57279 Jackson57279 merged commit 61c859e into master Nov 28, 2025
19 of 24 checks passed
@Jackson57279 Jackson57279 deleted the feature/workos-migration branch November 28, 2025 02:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant