Skip to content

migration better auth polar sh#139

Merged
Jackson57279 merged 2 commits intomasterfrom
migration-better-auth-polar-sh
Nov 12, 2025
Merged

migration better auth polar sh#139
Jackson57279 merged 2 commits intomasterfrom
migration-better-auth-polar-sh

Conversation

@Jackson57279
Copy link
Owner

@Jackson57279 Jackson57279 commented Nov 12, 2025

  • fun
  • Implement Better Auth and Polar.sh integration with Convex
  • error fixing
  • custom pop-up
  • Add comprehensive tech stack documentation with Mermaid diagrams
  • Restructure docs and implement email verification with rate limiting
  • im too lazy to fix shit
  • fixing mistakes flagged by coderabbit
  • Fix Better Auth Convex integration and complete Prisma migration
  • Add cleanup utility for orphaned projects with invalid userId references
  • changes

Summary by CodeRabbit

  • Bug Fixes

    • Resolved CORS issues affecting authentication across different deployment domains
    • Improved authentication endpoint security with origin validation and preflight request handling
  • New Features

    • Enhanced deployment flexibility with dynamic domain configuration support
    • Adaptive base URL detection for multiple environment scenarios
  • Chores

    • Added data validation and cleanup utilities for system maintenance

- Add cleanupOrphanedProjects mutation to identify and remove projects with userId references that don't exist in the users table
- Add cleanupOrphanedProjectsAction for HTTP-accessible cleanup
- Add cleanup-orphaned-projects.ts admin script for running cleanup from CLI
- Add comprehensive documentation in SCHEMA_VALIDATION_FIX.md
- Fixes schema validation errors that occur after Clerk to Better Auth migration
@codecapyai
Copy link

codecapyai bot commented Nov 12, 2025

CodeCapy Review ₍ᐢ•(ܫ)•ᐢ₎

Codebase Summary

This repository is a web application that integrates Better Auth with Convex as its backend database, along with Polar.sh integration. The project includes both frontend components (Next.js, React) and backend configurations (Convex mutations/queries). The application handles email/password authentication, social OAuth (Google, GitHub), session management, and custom error/CORS handling. Additional documentation and utility scripts, such as orphaned project cleanup, are provided.

PR Changes

The pull request implements multiple changes including:

  • Upgrading Better Auth integration with Convex and Polar.sh, updating schema validations to reference the users table correctly (e.g. userId field now using v.id('users')).
  • Adding a custom authentication pop-up component and associated UI changes.
  • Implementing email verification configuration and rate limiting for auth endpoints.
  • Introducing a cleanup utility for orphaned projects and related data to fix schema validation errors.
  • Changes to server configuration to support dynamic base URLs and proper CORS headers in the API routes.
  • Extensive documentation updates including detailed integration guides, quick references, CORS and domain configuration instructions, and migration guides.

Setup Instructions

  1. Install Node.js and pnpm globally if not already installed (e.g., sudo npm install -g pnpm).
  2. Clone the repository and navigate into it: cd .
  3. Install dependencies by running: pnpm install
  4. Start the development server with: pnpm dev
  5. Open your web browser and navigate to: http://localhost:3000
  6. Ensure that your environment variables (e.g., BETTER_AUTH_URL, NEXT_PUBLIC_APP_URL, and OAuth credentials) are configured as per the updated documentation in env.example and the explanations folder.

Generated Test Cases

1: Email/Password Sign-Up Flow ❗️❗️❗️

Description: Tests the complete sign-up process using email and password. Ensures that after filling the sign-up form, the user is properly created and redirected to the dashboard with an active session.

Prerequisites:

Steps:

  1. Start the dev server and open the web application at 'http://localhost:3000'.
  2. Click on the 'Sign Up' button or link on the homepage.
  3. Fill in the sign-up form with a valid email, password, and optional name.
  4. Submit the form by clicking the 'Submit' or 'Register' button.
  5. Wait for the page to redirect to the dashboard or profile screen.
  6. Verify that the dashboard displays a welcome message including the user's name.

Expected Result: The user is successfully registered, a new account is created in the backend, and the dashboard displays a personalized welcome message. A session token is stored and the user remains logged in.

2: Email/Password Sign-In Flow ❗️❗️❗️

Description: Validates that an existing user can log in using email and password and that the UI updates to reflect the authenticated state.

Prerequisites:

  • A user account already exists via the sign-up process.

Steps:

  1. Start the dev server and navigate to 'http://localhost:3000'.
  2. Click on the 'Sign In' button or link on the homepage.
  3. Enter the correct email and password for the existing user.
  4. Click the 'Sign In' button.
  5. Observe the redirection to the dashboard or authenticated area.
  6. Check that the header or profile area displays the user’s name and a logout option.

Expected Result: The user is successfully authenticated, and the UI reflects the logged-in state (e.g., welcome message, profile info visible, and logout button displayed).

3: OAuth Sign-In Flow (Google) ❗️❗️❗️

Description: Checks the OAuth workflow by triggering a sign-in with a social provider (Google). Validates redirection and session creation when authentication is successful.

Prerequisites:

  • OAuth environment variables (e.g., GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET) are correctly configured in the environment.

Steps:

  1. Open the web application at 'http://localhost:3000'.
  2. Click the 'Sign in with Google' button, which is part of the custom auth pop-up or main sign-in options.
  3. Simulate the redirection to the Google consent screen and approve the permissions (this step may be stubbed or mocked in a test environment).
  4. After redirection back from Google, observe that the application processes the OAuth callback.
  5. The page should redirect to the dashboard and display the user's profile information.

Expected Result: The OAuth sign-in process completes successfully, the user is authenticated, and session information is visible in the UI.

4: Custom Authentication Pop-Up Display ❗️❗️

Description: Ensures that the custom pop-up for authentication is triggered when initiating a sign-in and that it displays all necessary elements.

Prerequisites:

  • User is not logged in.

Steps:

  1. Open the application at 'http://localhost:3000'.
  2. Click on the 'Sign In' button that is expected to open a custom pop-up (verify that the pop-up component from src/components/auth/auth-popup.tsx is used).
  3. Observe the animation or appearance of the pop-up.
  4. Verify that the pop-up contains input fields for email and password and options for social sign-in.
  5. Click the close button on the pop-up to dismiss it.

Expected Result: The custom authentication pop-up is displayed with all required input fields and buttons, then properly dismissed when closed.

5: Session Persistence After Page Refresh ❗️❗️❗️

Description: Verifies that after a user has signed in, refreshing the page retains the session and the authenticated state.

Prerequisites:

  • User is already logged in via either email/password or OAuth sign-in.

Steps:

  1. While logged in, refresh the browser page (using the browser refresh button or F5).
  2. Wait for the page to load completely.
  3. Check the header, profile section, or dashboard to confirm that user-specific details are still displayed.

Expected Result: The user remains logged in after a page refresh, with persistent session data displayed in the UI.

6: Error Handling When Wrong Password is Entered ❗️❗️

Description: Tests the scenario where a user attempts to sign in with incorrect credentials and observes an error message.

Prerequisites:

  • A user account exists from a previous sign-up.

Steps:

  1. Navigate to the sign-in page at 'http://localhost:3000'.
  2. Enter the correct email but an incorrect password.
  3. Click on the 'Sign In' button.
  4. Observe the UI response.

Expected Result: An error message is displayed indicating that the credentials are incorrect, and the UI remains on the sign-in screen without redirecting.

7: Navigation and Layout Check for Auth Pages ❗️

Description: Ensures that the overall layout, including header, footer, and authentication buttons, is rendered correctly on the sign-in and sign-up pages.

Prerequisites:

Steps:

  1. Open the application at 'http://localhost:3000'.
  2. Navigate to both the sign-up and sign-in pages.
  3. Visually inspect the layout to ensure that key components such as the header, footer, and auth buttons appear in their expected positions.
  4. Confirm that no visual errors or misalignments are present.

Expected Result: The UI is correctly laid out, with all elements (header, footer, forms, buttons) properly aligned and styled as per design specifications.

Raw Changes Analyzed
File: convex/importData.ts
Changes:
@@ -14,7 +14,7 @@ export const importProject = internalMutation({
   args: {
     oldId: v.string(), // Original PostgreSQL UUID
     name: v.string(),
-    userId: v.id("users"), // Changed from v.string() to v.id("users")
+    userId: v.id("users"), // References users table
     framework: v.union(
       v.literal("NEXTJS"),
       v.literal("ANGULAR"),
@@ -202,7 +202,7 @@ export const importAttachment = internalMutation({
 export const importUsage = internalMutation({
   args: {
     key: v.string(), // Original key like "rlflx:user_XXX"
-    userId: v.id("users"), // Changed from v.string() to v.id("users")
+    userId: v.id("users"), // References users table
     points: v.number(),
     expire: v.optional(v.string()), // ISO date string
   },
@@ -275,12 +275,125 @@ export const clearAllData = internalMutation({
   },
 });
 
+/**
+ * Clean up orphaned projects and related data (projects with invalid userId references)
+ * This fixes schema validation errors when users table is missing references
+ */
+export const cleanupOrphanedProjects = internalMutation({
+  args: {},
+  handler: async (ctx) => {
+    const allUsers = await ctx.db.query("users").collect();
+    const validUserIds = new Set(allUsers.map((u) => u._id as string));
+
+    let cleanedProjectsCount = 0;
+    let cleanedUsageCount = 0;
+    let cleanedOAuthCount = 0;
+    let cleanedImportsCount = 0;
+    const orphanedProjectIds: string[] = [];
+
+    // 1. Find all orphaned projects
+    const allProjects = await ctx.db.query("projects").collect();
+    for (const project of allProjects) {
+      if (!validUserIds.has(project.userId as any)) {
+        orphanedProjectIds.push(project._id);
+      }
+    }
+
+    // Delete all data related to orphaned projects in reverse dependency order
+    for (const projectId of orphanedProjectIds) {
+      // Delete attachments for messages in this project
+      const messages = await ctx.db
+        .query("messages")
+        .withIndex("by_projectId", (q) => q.eq("projectId", projectId as any))
+        .collect();
+
+      for (const message of messages) {
+        const attachments = await ctx.db
+          .query("attachments")
+          .withIndex("by_messageId", (q) => q.eq("messageId", message._id))
+          .collect();
+
+        for (const attachment of attachments) {
+          await ctx.db.delete(attachment._id);
+        }
+
+        // Delete fragments for this message
+        const fragments = await ctx.db
+          .query("fragments")
+          .withIndex("by_messageId", (q) => q.eq("messageId", message._id))
+          .collect();
+
+        for (const fragment of fragments) {
+          await ctx.db.delete(fragment._id);
+        }
+
+        // Delete the message itself
+        await ctx.db.delete(message._id);
+      }
+
+      // Delete fragment drafts for this project
+      const drafts = await ctx.db
+        .query("fragmentDrafts")
+        .withIndex("by_projectId", (q) => q.eq("projectId", projectId as any))
+        .collect();
+
+      for (const draft of drafts) {
+        await ctx.db.delete(draft._id);
+      }
+
+      // Delete the project itself
+      await ctx.db.delete(projectId as any);
+      cleanedProjectsCount++;
+    }
+
+    // 2. Clean up orphaned usage records
+    const allUsage = await ctx.db.query("usage").collect();
+    for (const usage of allUsage) {
+      if (!validUserIds.has(usage.userId as any)) {
+        await ctx.db.delete(usage._id);
+        cleanedUsageCount++;
+      }
+    }
+
+    // 3. Clean up orphaned oauthConnections
+    const allOAuth = await ctx.db.query("oauthConnections").collect();
+    for (const oauth of allOAuth) {
+      if (!validUserIds.has(oauth.userId as any)) {
+        await ctx.db.delete(oauth._id);
+        cleanedOAuthCount++;
+      }
+    }
+
+    // 4. Clean up orphaned imports
+    const allImports = await ctx.db.query("imports").collect();
+    for (const importRecord of allImports) {
+      if (!validUserIds.has(importRecord.userId as any)) {
+        await ctx.db.delete(importRecord._id);
+        cleanedImportsCount++;
+      }
+    }
+
+    const totalCleaned = cleanedProjectsCount + cleanedUsageCount + cleanedOAuthCount + cleanedImportsCount;
+
+    return {
+      success: true,
+      message: `Cleaned up ${totalCleaned} orphaned records (${cleanedProjectsCount} projects, ${cleanedUsageCount} usage, ${cleanedOAuthCount} oauth, ${cleanedImportsCount} imports)`,
+      cleanedProjectCount: cleanedProjectsCount,
+      cleanedUsageCount,
+      cleanedOAuthCount,
+      cleanedImportsCount,
+      totalCleaned,
+      orphanedProjectIds,
+    };
+  },
+});
+
 // Public action wrappers for HTTP client access
 export const importProjectAction = action({
   args: {
     oldId: v.string(),
     name: v.string(),
-    userId: v.id("users"), // Changed from v.string() to v.id("users")
+    userId: v.id("users"), // References users table
     framework: v.union(
       v.literal("NEXTJS"),
       v.literal("ANGULAR"),
@@ -382,11 +495,31 @@ export const importAttachmentAction = action({
 export const importUsageAction = action({
   args: {
     key: v.string(),
-    userId: v.id("users"), // Changed from v.string() to v.id("users")
+    userId: v.id("users"), // References users table
     points: v.number(),
     expire: v.optional(v.string()),
   },
   handler: async (ctx, args): Promise<{ userId: string; newId: any }> => {
     return await ctx.runMutation(internal.importData.importUsage, args);
   },
 });
+
+/**
+ * Public action to clean up ALL orphaned data (admin function)
+ * Removes all projects, usage, oauthConnections, imports with invalid userId references
+ */
+export const cleanupOrphanedProjectsAction = action({
+  args: {},
+  handler: async (ctx): Promise<{
+    success: boolean;
+    message: string;
+    cleanedProjectCount: number;
+    cleanedUsageCount: number;
+    cleanedOAuthCount: number;
+    cleanedImportsCount: number;
+    totalCleaned: number;
+    orphanedProjectIds: string[];
+  }> => {
+    return await ctx.runMutation(internal.importData.cleanupOrphanedProjects);
+  },
+});

File: convex/schema.ts
Changes:
@@ -110,7 +110,7 @@ export default defineSchema({
   // Projects table
   projects: defineTable({
     name: v.string(),
-    userId: v.id("users"), // Changed to reference users table
+    userId: v.id("users"), // References users table
     framework: frameworkEnum,
     modelPreference: v.optional(v.string()), // User's preferred AI model (e.g., "auto", "anthropic/claude-haiku-4.5", "openai/gpt-4o")
     createdAt: v.optional(v.number()), // timestamp
@@ -175,7 +175,7 @@ export default defineSchema({
 
   // OAuth Connections table - for storing encrypted OAuth tokens
   oauthConnections: defineTable({
-    userId: v.id("users"), // Changed to reference users table
+    userId: v.id("users"), // References users table
     provider: oauthProviderEnum,
     accessToken: v.string(), // Encrypted token
     refreshToken: v.optional(v.string()),
@@ -190,7 +190,7 @@ export default defineSchema({
 
   // Imports table - tracking import history and status
   imports: defineTable({
-    userId: v.id("users"), // Changed to reference users table
+    userId: v.id("users"), // References users table
     projectId: v.id("projects"),
     messageId: v.optional(v.id("messages")),
     source: importSourceEnum,
@@ -209,7 +209,7 @@ export default defineSchema({
 
   // Usage table - rate limiting and credit tracking
   usage: defineTable({
-    userId: v.id("users"), // Changed to reference users table
+    userId: v.id("users"), // References users table
     points: v.number(), // Remaining credits
     expire: v.optional(v.number()), // Expiration timestamp
     planType: v.optional(v.union(v.literal("free"), v.literal("pro"))), // Track plan type

File: env.example
Changes:
@@ -8,7 +8,7 @@ CONVEX_DEPLOYMENT=""
 
 # Better Auth
 BETTER_AUTH_SECRET="" # Generate with: openssl rand -base64 32
-BETTER_AUTH_URL="http://localhost:3000" # Use production URL outside of local dev
+BETTER_AUTH_URL="http://localhost:3000" # IMPORTANT: Must match your deployment domain (e.g., https://zapdev.link)
 SESSION_COOKIE_PREFIX="zapdev" # Optional Better Auth cookie prefix override
 SESSION_COOKIE_NAME="" # Optional full cookie name override (defaults to "<prefix>.session_token")
 

File: explanations/BETTER_AUTH_CONVEX_INTEGRATION.md
Changes:
@@ -0,0 +1,667 @@
+# Better Auth + Convex Integration Guide
+
+This guide explains how Better Auth and Convex are linked together in ZapDev, and what each component does.
+
+## Overview
+
+Better Auth is a modern authentication framework that needs a database to store users, sessions, and OAuth accounts. Instead of using a traditional SQL database, we use **Convex** (a real-time database) as the backend.
+
+The connection is made through a **Custom Database Adapter** that implements Better Auth's `DatabaseAdapter` interface, routing all auth operations to Convex mutations and queries.
+
+## Architecture
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│                    Frontend (React/Next.js)                  │
+│  - authClient.useSession()                                   │
+│  - authClient.signIn() / signUp() / signOut()               │
+└────────────────────────┬────────────────────────────────────┘
+
+
+┌─────────────────────────────────────────────────────────────┐
+│           API Routes (/api/auth/[...all]/route.ts)           │
+│  - POST: Handle sign-up, sign-in, OAuth callbacks           │
+│  - GET: OAuth provider redirects                             │
+│  - Rate limiting enabled                                     │
+└────────────────────────┬────────────────────────────────────┘
+
+
+┌─────────────────────────────────────────────────────────────┐
+│         Better Auth (src/lib/auth.ts)                        │
+│  - Email/password authentication                             │
+│  - OAuth providers (Google, GitHub)                          │
+│  - Session management (7 days default)                       │
+│  - Database: createConvexAdapter()                           │
+└────────────────────────┬────────────────────────────────────┘
+
+
+┌─────────────────────────────────────────────────────────────┐
+│   Convex Adapter (src/lib/auth-adapter-convex.ts)            │
+│  - Implements DatabaseAdapter interface                      │
+│  - Maps Better Auth calls to Convex operations              │
+│  - OAuth token refresh logic                                 │
+└────────────────────────┬────────────────────────────────────┘
+
+        ┌────────────────┼────────────────┐
+        │                │                │
+        ▼                ▼                ▼
+┌──────────────┐  ┌──────────────┐  ┌──────────────┐
+│ Users Module │  │ Sessions     │  │ Accounts     │
+│ (users.ts)   │  │ Module       │  │ Module       │
+│              │  │ (sessions.ts)│  │ (accounts.ts)│
+└──────┬───────┘  └──────┬───────┘  └──────┬───────┘
+       │                 │                │
+       └─────────────────┼─────────────────┘
+
+
+        ┌────────────────────────────────┐
+        │    Convex Database Tables      │
+        │  - users                       │
+        │  - sessions                    │
+        │  - accounts (OAuth)            │
+        │  - emailVerifications          │
+        └────────────────────────────────┘
+```
+
+## Database Tables
+
+### 1. Users Table (`users`)
+
+Stores user account information and subscription data.
+
+```typescript
+{
+  email: string;              // Unique email
+  emailVerified: boolean;     // Email verification status
+  name?: string;              // User's full name
+  image?: string;             // Avatar URL
+  
+  // Polar.sh subscription fields
+  polarCustomerId?: string;   // Linked to Polar account
+  subscriptionId?: string;    // Active subscription ID
+  subscriptionStatus?: string; // "active", "canceled", "past_due", etc.
+  plan?: "free" | "pro";      // Current plan tier
+  
+  createdAt: number;          // Timestamp
+  updatedAt: number;          // Timestamp
+}
+
+// Indexes
+.index("by_email", ["email"])
+.index("by_polarCustomerId", ["polarCustomerId"])
+```
+
+**When records are created**:
+- User signs up with email/password
+- User signs in with OAuth (Google/GitHub)
+- Better Auth automatically creates/updates via adapter
+
+### 2. Sessions Table (`sessions`)
+
+Manages user login sessions with expiration and renewal.
+
+```typescript
+{
+  userId: Id<"users">;        // Reference to user
+  expiresAt: number;          // Session expiration timestamp
+  token: string;              // Session token (httpOnly cookie)
+  ipAddress?: string;         // Client IP for security
+  userAgent?: string;         // Browser info
+}
+
+// Indexes
+.index("by_userId", ["userId"])
+.index("by_token", ["token"])
+```
+
+**Session lifecycle**:
+1. User logs in → `sessions.create()` called
+2. Session stored with 7-day expiration
+3. Each request updates `updatedAt` 
+4. If `updatedAt + updateAge < now`, session is refreshed automatically
+5. Expired sessions returned as `null` by `getByToken()`
+6. User logs out → `sessions.deleteByToken()` called
+
+### 3. Accounts Table (`accounts`)
+
+Stores OAuth provider connections (Google, GitHub, etc.).
+
+```typescript
+{
+  userId: Id<"users">;                    // User this account belongs to
+  provider: string;                       // "google" or "github"
+  providerAccountId: string;              // Provider's user ID
+  accessToken?: string;                   // OAuth access token
+  refreshToken?: string;                  // OAuth refresh token
+  expiresAt?: number;                     // Token expiration
+  tokenType?: string;                     // "Bearer", etc.
+  scope?: string;                         // Permission scope
+  idToken?: string;                       // OpenID Connect ID token
+}
+
+// Indexes
+.index("by_provider_accountId", ["provider", "providerAccountId"])
+.index("by_userId", ["userId"])
+```
+
+**OAuth flow**:
+1. User clicks "Sign in with Google"
+2. Redirected to Google consent screen
+3. Google redirects back with authorization code
+4. Better Auth exchanges code for tokens
+5. Adapter stores in `accounts` table
+6. On next request, tokens are checked for expiration
+7. If expired, automatically refreshed using `refreshToken`
+
+### 4. Email Verifications Table (`emailVerifications`)
+
+Optional: Stores email verification tokens.
+
+```typescript
+{
+  userId: Id<"users">;
+  email: string;
+  token: string;              // Unique verification token
+  expiresAt: number;          // Token expiration
+  verified: boolean;          // Verification status
+  createdAt: number;
+}
+
+// Indexes
+.index("by_token", ["token"])
+.index("by_userId", ["userId"])
+.index("by_email", ["email"])
+```
+
+Currently **email verification is disabled** in `src/lib/auth.ts`:
+```typescript
+emailAndPassword: {
+  enabled: true,
+  requireEmailVerification: false, // ← Set to true to enable
+}
+```
+
+## Convex Modules
+
+### users.ts - User Account Management
+
+**Key functions**:
+
+| Function | Purpose | When Used |
+|----------|---------|-----------|
+| `createOrUpdate(email, name, image, emailVerified)` | Create new user or update existing | Sign-up, OAuth login |
+| `getByEmail(email)` | Find user by email | Login lookup |
+| `getById(userId)` | Get user by ID | Profile fetch |
+| `update(userId, updates)` | Update user profile | Change name/avatar |
+| `deleteUser(userId)` | Delete user and cascade | Account deletion |
+| `linkPolarCustomer(userId, polarCustomerId)` | Link Polar subscription | Subscription created |
+| `unlinkPolarCustomer(userId, expectedPolarCustomerId)` | Unlink Polar | Subscription canceled |
+| `updateSubscription(polarCustomerId, subscriptionId, status, plan)` | Update subscription | Polar webhook |
+
+### sessions.ts - Session Lifecycle
+
+**Key functions**:
+
+| Function | Purpose | When Used |
+|----------|---------|-----------|
+| `create(userId, expiresAt, token, ipAddress, userAgent)` | Create session | Successful login |
+| `getByToken(token)` | Get session by token | Auth middleware |
+| `getByUserId(userId)` | Get all active sessions | Admin, device list |
+| `updateByToken(token, expiresAt)` | Refresh session | Session renewal |
+| `deleteByToken(token)` | Delete session | Logout |
+| `deleteByUserId(userId)` | Delete all sessions | Account deletion |
+| `cleanupExpired()` | Remove expired sessions | Scheduled job |
+
+### accounts.ts - OAuth Integration
+
+**Key functions**:
+
+| Function | Purpose | When Used |
+|----------|---------|-----------|
+| `create(userId, provider, providerAccountId, tokens)` | Store OAuth account | OAuth sign-up/login |
+| `getByProvider(provider, providerAccountId)` | Retrieve OAuth account | Token refresh check |
+| `getByUserId(userId)` | Get all OAuth accounts | Connected apps list |
+| `update(provider, providerAccountId, tokens)` | Update tokens | Token refresh |
+| `deleteOAuth(provider, providerAccountId)` | Disconnect provider | Revoke OAuth |
+| `deleteByUserId(userId)` | Delete all accounts | Account deletion |
+
+## Better Auth Configuration
+
+### src/lib/auth.ts
+
+Main Better Auth instance:
+
+```typescript
+export const auth = betterAuth({
+  // Base URL for auth endpoints
+  baseURL: "http://localhost:3000", // dev
+  // or "https://zapdev-mu.vercel.app" // prod
+  
+  // Use Convex as database
+  database: createConvexAdapter(),
+  
+  // Email & password authentication
+  emailAndPassword: {
+    enabled: true,
+    requireEmailVerification: false,  // Change to true to enable
+  },
+  
+  // Social providers (configured from env vars)
+  socialProviders: {
+    google: { clientId, clientSecret },
+    github: { clientId, clientSecret }
+  },
+  
+  // Session configuration
+  session: {
+    expiresIn: 7 * 24 * 60 * 60,  // 7 days
+    updateAge: 24 * 60 * 60,      // Refresh after 1 day
+    cookieCache: {
+      enabled: true,
+      maxAge: 60 * 60              // Cache for 1 hour
+    }
+  },
+  
+  // Advanced options
+  advanced: {
+    cookiePrefix: "zapdev.",
+    disableCSRFProtection: false,
+  },
+  
+  plugins: [nextCookies()]  // Next.js cookie plugin
+});
+```
+
+**Key settings explained**:
+
+| Setting | Default | Purpose |
+|---------|---------|---------|
+| `expiresIn` | 7 days | Session validity period |
+| `updateAge` | 1 day | Refresh threshold (if last update > 1 day, refresh) |
+| `cookieCache.enabled` | true | Cache session in memory for faster lookups |
+| `cookieCache.maxAge` | 1 hour | Invalidate cache after 1 hour |
+| `cookiePrefix` | "zapdev." | Cookie name prefix (e.g., "zapdev.session_token") |
+| `requireEmailVerification` | false | Require email verification before login |
+
+## Database Adapter
+
+### src/lib/auth-adapter-convex.ts
+
+This file is the **bridge** between Better Auth and Convex. It implements the `DatabaseAdapter` interface with these methods:
+
+#### User Operations
+```typescript
+async createUser(user): Promise<User>
+async getUser(id): Promise<User | null>
+async getUserByEmail(email): Promise<User | null>
+async updateUser(id, updates): Promise<User | null>
+async deleteUser(id): Promise<boolean>
+```
+
+Maps to Convex mutations:
+- `api.users.createOrUpdate`
+- `api.users.getById`
+- `api.users.getByEmail`
+- `api.users.update`
+- `api.users.deleteUser`
+
+#### Session Operations
+```typescript
+async createSession(session): Promise<Session>
+async getSession(token): Promise<Session | null>
+async updateSession(token, updates): Promise<Session | null>
+async deleteSession(token): Promise<boolean>
+```
+
+Maps to Convex mutations:
+- `api.sessions.create`
+- `api.sessions.getByToken`
+- `api.sessions.updateByToken`
+- `api.sessions.deleteByToken`
+
+#### OAuth Operations
+```typescript
+async createAccount(account): Promise<Account>
+async getAccount(provider, providerAccountId): Promise<Account | null>
+async updateAccount(provider, providerAccountId, updates): Promise<Account | null>
+async deleteAccount(provider, providerAccountId): Promise<boolean>
+```
+
+Maps to Convex mutations:
+- `api.accounts.create`
+- `api.accounts.getByProvider`
+- `api.accounts.update`
+- `api.accounts.deleteOAuth`
+
+**Special feature**: The `getAccount()` method automatically detects expired OAuth tokens and refreshes them using the `refreshToken`:
+
+```typescript
+// In getAccount():
+if (isOAuthTokenExpired(expiresAt) && refreshToken) {
+  const refreshResult = await refreshOAuthTokenForProvider(
+    provider,
+    refreshToken,
+    clientId,
+    clientSecret
+  );
+  // Update database with new token
+  await this.updateAccount(provider, providerAccountId, {
+    accessToken: refreshResult.accessToken,
+    expiresAt: Date.now() + refreshResult.expiresIn * 1000
+  });
+}
+```
+
+## API Routes
+
+### src/app/api/auth/[...all]/route.ts
+
+Handles all authentication HTTP requests:
+
+```typescript
+export async function POST(request: Request) {
+  // Rate limiting check
+  const rateLimitResult = await checkRateLimit(request);
+  if (!rateLimitResult.success) {
+    return rateLimitResult.response;
+  }
+  
+  // Route to Better Auth handler
+  const handlers = toNextJsHandler(auth);
+  return handlers.POST(request);
+}
+
+export async function GET(request: Request) {
+  // OAuth callbacks don't need rate limiting
+  const handlers = toNextJsHandler(auth);
+  return handlers.GET(request);
+}
+```
+
+**Endpoints created automatically by Better Auth**:
+
+| Endpoint | Method | Purpose |
+|----------|--------|---------|
+| `/api/auth/sign-up` | POST | Register with email/password |
+| `/api/auth/sign-in` | POST | Login with email/password |
+| `/api/auth/sign-out` | POST | Logout (invalidate session) |
+| `/api/auth/session` | GET | Get current session |
+| `/api/auth/signin/google` | GET | Start Google OAuth |
+| `/api/auth/callback/google` | GET | Handle Google redirect |
+| `/api/auth/signin/github` | GET | Start GitHub OAuth |
+| `/api/auth/callback/github` | GET | Handle GitHub redirect |
+
+**Rate limiting** is applied to sign-up/login endpoints to prevent brute force attacks.
+
+## Client Integration
+
+### src/lib/auth-client.ts
+
+Frontend auth client:
+
+```typescript
+export const authClient = createAuthClient({
+  baseURL: "http://localhost:3000"
+});
+
+export const {
+  signIn,
+  signUp,
+  signOut,
+  useSession,
+} = authClient;
+```
+
+**Usage in React components**:
+
+```typescript
+import { useSession, signIn, signOut } from "@/lib/auth-client";
+
+function Profile() {
+  const { data: session } = useSession();
+  
+  if (!session) {
+    return <div>Not logged in</div>;
+  }
+  
+  return (
+    <div>
+      <p>Welcome, {session.user.name}!</p>
+      <button onClick={() => signOut()}>Logout</button>
+    </div>
+  );
+}
+```
+
+## Data Flow Examples
+
+### 1. Email/Password Sign-Up
+
+```
+User clicks "Sign Up"
+
+Form submits email & password
+
+POST /api/auth/sign-up
+
+Better Auth validates
+
+Convex Adapter: createUser()
+
+Convex Mutation: users.createOrUpdate()
+
+Insert into users table
+
+Convex Adapter: createSession()
+
+Convex Mutation: sessions.create()
+
+Insert into sessions table
+
+Response: { token, user }
+
+Client stores in httpOnly cookie
+
+React re-renders with useSession()
+```
+
+### 2. OAuth Sign-In (Google)
+
+```
+User clicks "Sign in with Google"
+
+GET /api/auth/signin/google
+
+Better Auth: redirect to Google consent screen
+
+User authorizes
+
+Google redirects to /api/auth/callback/google?code=...
+
+Better Auth exchanges code for access token
+
+Convex Adapter: getUserByEmail()
+
+If user exists:
+  ├─ Convex Adapter: getAccount()
+  ├─ Check if OAuth token is expired
+  ├─ If expired, refresh automatically
+  ├─ Convex Adapter: getSession() (existing session)
+  └─ Return existing session
+
+If user not found:
+  ├─ Convex Adapter: createUser()
+  ├─ Convex Adapter: createAccount() (store OAuth tokens)
+  ├─ Convex Adapter: createSession()
+  └─ Return new session
+
+Client receives session token in httpOnly cookie
+```
+
+### 3. Session Validation on Page Load
+
+```
+User loads app
+
+Browser sends request with session cookie
+
+Frontend calls useSession()
+
+Better Auth reads cookie
+
+Convex Adapter: getSession(token)
+
+Query sessions table
+
+If expiresAt < now: return null (expired)
+
+If expiresAt > now && (updatedAt + updateAge < now):
+  ├─ Convex Adapter: updateSession()
+  ├─ Set new expiresAt
+  └─ Return updated session
+
+Client receives session with user data
+```
+
+## Environment Variables
+
+Required for Better Auth + Convex to work:
+
+```bash
+# Application URL
+NEXT_PUBLIC_APP_URL=http://localhost:3000
+
+# Better Auth
+BETTER_AUTH_SECRET=<generate with: openssl rand -base64 32>
+BETTER_AUTH_URL=http://localhost:3000
+
+# Convex
+NEXT_PUBLIC_CONVEX_URL=<your-convex-url>
+CONVEX_DEPLOYMENT=<deployment-id>
+
+# OAuth Providers (optional)
+GOOGLE_CLIENT_ID=<from Google Console>
+GOOGLE_CLIENT_SECRET=<from Google Console>
+GITHUB_CLIENT_ID=<from GitHub Settings>
+GITHUB_CLIENT_SECRET=<from GitHub Settings>
+
+# Polar.sh (billing)
+POLAR_ACCESS_TOKEN=<token>
+POLAR_ORGANIZATION_ID=<id>
+NEXT_PUBLIC_POLAR_PRODUCT_ID_PRO=<product-id>
+POLAR_WEBHOOK_SECRET=<secret>
+```
+
+See `.env.example` and `explanations/BETTER_AUTH_POLAR_SETUP.md` for complete setup.
+
+## Advanced: Email Verification
+
+To enable email verification:
+
+1. **Update Better Auth config** (`src/lib/auth.ts`):
+```typescript
+emailAndPassword: {
+  enabled: true,
+  requireEmailVerification: true  // ← Change this
+}
+```
+
+2. **Setup email provider** (nodemailer, SendGrid, Resend, etc.):
+```typescript
+import { sendEmailVerificationEmail } from "@/lib/email";
+
+// In auth config
+emailAndPassword: {
+  enabled: true,
+  requireEmailVerification: true,
+  sendVerificationEmail: async (email, token) => {
+    await sendEmailVerificationEmail(email, token);
+  }
+}
+```
+
+3. **Verify email endpoint**:
+```typescript
+// POST /api/auth/verify-email?token=...
+// Better Auth handles this automatically
+```
+
+The `convex/emailVerifications.ts` module provides:
+- `createVerification()` - Generate token
+- `getByToken()` - Retrieve verification
+- `markVerified()` - Mark email as verified
+- `deleteExpired()` - Cleanup old tokens
+
+## Troubleshooting
+
+### Session not persisting across page reload
+**Cause**: Session token not in httpOnly cookie
+**Fix**: Check `BETTER_AUTH_URL` and `NEXT_PUBLIC_APP_URL` match exactly
+
+### OAuth token refresh not working
+**Cause**: OAuth credentials missing or incorrect
+**Fix**: Verify `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, etc. in environment
+
+### "User not found" errors
+**Cause**: Convex adapter failing to create user
+**Fix**: Check network tab → `/api/auth/sign-up` response for error details
+
+### Sessions expiring too quickly
+**Cause**: `expiresIn` set to low value
+**Fix**: Adjust in `src/lib/auth.ts` → `session.expiresIn`
+
+### Rate limiting blocking legitimate requests
+**Cause**: Too many auth attempts from same IP
+**Fix**: Whitelist IP or adjust `checkRateLimit()` threshold in rate-limit.ts
+
+## Testing the Integration
+
+### Manual test: Email/Password Sign-Up
+```bash
+# 1. Start dev server
+bun run dev
+bun run convex:dev  # in another terminal
+
+# 2. Open http://localhost:3000
+# 3. Click "Sign Up"
+# 4. Enter email & password
+# 5. Check Convex dashboard:
+#    - users table should have new entry
+#    - sessions table should have new session
+# 6. Refresh page - session should persist
+```
+
+### Manual test: Google OAuth
+```bash
+# 1. Configure GOOGLE_CLIENT_ID & GOOGLE_CLIENT_SECRET in .env.local
+# 2. Start dev server
+# 3. Click "Sign in with Google"
+# 4. Authorize
+# 5. Check Convex dashboard:
+#    - users table has new user
+#    - accounts table has Google connection
+# 6. Sign out and sign in again - should use existing session
+```
+
+### Manual test: Session Refresh
+```bash
+# 1. Sign in
+# 2. Keep browser open for > 1 day (or manually set expiresAt to 1 day ago)
+# 3. Make a request (refresh page)
+# 4. Check Convex: sessions table expiresAt should update to future date
+```
+
+## Security Considerations
+
+1. **Session tokens**: Stored in httpOnly cookies (secure by default)
+2. **CSRF protection**: Enabled by default in Better Auth
+3. **Password hashing**: Better Auth uses bcrypt automatically
+4. **OAuth tokens**: Never exposed in client-side code
+5. **Rate limiting**: POST endpoints protected against brute force
+6. **Email verification**: Optional but recommended for production
+
+## References
+
+- [Better Auth Documentation](https://www.better-auth.com)
+- [Convex Documentation](https://docs.convex.dev)
+- [Next.js Authentication Best Practices](https://nextjs.org/docs/authentication)

File: explanations/BETTER_AUTH_CONVEX_QUICK_REFERENCE.md
Changes:
@@ -0,0 +1,402 @@
+# Better Auth + Convex: Quick Reference
+
+A cheat sheet for working with authentication in ZapDev.
+
+## File Map
+
+| File | Purpose |
+|------|---------|
+| `src/lib/auth.ts` | Better Auth instance config |
+| `src/lib/auth-client.ts` | Frontend auth hooks |
+| `src/lib/auth-adapter-convex.ts` | Bridge between Better Auth & Convex |
+| `src/app/api/auth/[...all]/route.ts` | API endpoint handler |
+| `convex/schema.ts` | Database tables |
+| `convex/users.ts` | User CRUD functions |
+| `convex/sessions.ts` | Session management |
+| `convex/accounts.ts` | OAuth connections |
+| `convex/emailVerifications.ts` | Email verification (optional) |
+
+## Frontend Usage
+
+### Get Current Session
+```typescript
+import { useSession } from "@/lib/auth-client";
+
+function Component() {
+  const { data: session, isPending } = useSession();
+  
+  if (isPending) return <div>Loading...</div>;
+  if (!session) return <div>Not logged in</div>;
+  
+  return <div>Welcome {session.user.name}!</div>;
+}
+```
+
+### Sign In
+```typescript
+import { signIn } from "@/lib/auth-client";
+
+async function LoginForm() {
+  const { data, error } = await signIn.email({
+    email: "user@example.com",
+    password: "password123",
+    onSuccess: () => {
+      // Redirect to dashboard
+      window.location.href = "/dashboard";
+    },
+  });
+  
+  if (error) console.error(error.message);
+}
+```
+
+### Sign Up
+```typescript
+import { signUp } from "@/lib/auth-client";
+
+async function RegisterForm() {
+  const { data, error } = await signUp.email({
+    email: "newuser@example.com",
+    password: "securepassword",
+    name: "John Doe",
+    onSuccess: () => {
+      window.location.href = "/dashboard";
+    },
+  });
+  
+  if (error) console.error(error.message);
+}
+```
+
+### Sign Out
+```typescript
+import { signOut } from "@/lib/auth-client";
+
+async function LogoutButton() {
+  const { error } = await signOut();
+  if (error) console.error(error);
+}
+```
+
+### OAuth Sign-In
+```typescript
+import { signIn } from "@/lib/auth-client";
+
+async function GoogleLoginButton() {
+  // Automatically redirects to Google
+  await signIn.social({
+    provider: "google",
+  });
+}
+
+async function GitHubLoginButton() {
+  // Automatically redirects to GitHub
+  await signIn.social({
+    provider: "github",
+  });
+}
+```
+
+## Backend Usage
+
+### Get Current User (Server-Side)
+```typescript
+import { auth } from "@/lib/auth";
+
+export async function GET(request: Request) {
+  const session = await auth.api.getSession({ headers: request.headers });
+  
+  if (!session) {
+    return new Response("Unauthorized", { status: 401 });
+  }
+  
+  return Response.json({ user: session.user });
+}
+```
+
+### Get User by Email (Convex)
+```typescript
+import { fetchQuery } from "convex/nextjs";
+import { api } from "@/convex/_generated/api";
+
+const user = await fetchQuery(api.users.getByEmail, {
+  email: "user@example.com"
+});
+```
+
+### Check if User Has OAuth Account
+```typescript
+const { data: session } = useSession();
+const accounts = await fetchQuery(api.accounts.getByUserId, {
+  userId: session.user.id
+});
+
+const hasGoogle = accounts.some(a => a.provider === "google");
+```
+
+### Get Session by Token
+```typescript
+const session = await fetchQuery(api.sessions.getByToken, {
+  token: "session_token_here"
+});
+
+if (session && session.expiresAt < Date.now()) {
+  console.log("Session expired");
+}
+```
+
+## Convex Patterns
+
+### List All Users (Dangerous - For Admin Only!)
+```typescript
+import { query } from "./_generated/server";
+
+export const getAllUsers = query({
+  args: {},
+  handler: async (ctx) => {
+    return await ctx.db.query("users").collect();
+  }
+});
+```
+
+### Check if Email is Taken
+```typescript
+import { query } from "./_generated/server";
+
+export const isEmailTaken = query({
+  args: { email: v.string() },
+  handler: async (ctx, args) => {
+    const user = await ctx.db
+      .query("users")
+      .withIndex("by_email", q => q.eq("email", args.email))
+      .first();
+    
+    return !!user;
+  }
+});
+```
+
+### Update User Profile
+```typescript
+import { fetchMutation } from "convex/nextjs";
+import { api } from "@/convex/_generated/api";
+
+const userId = session.user.id;
+await fetchMutation(api.users.update, {
+  userId,
+  name: "New Name",
+  image: "https://example.com/avatar.jpg"
+});
+```
+
+### Disconnect OAuth Provider
+```typescript
+import { fetchMutation } from "convex/nextjs";
+import { api } from "@/convex/_generated/api";
+
+await fetchMutation(api.accounts.deleteOAuth, {
+  provider: "google",
+  providerAccountId: "1234567890"
+});
+```
+
+### Delete User Account
+```typescript
+import { fetchMutation } from "convex/nextjs";
+import { api } from "@/convex/_generated/api";
+
+await fetchMutation(api.users.deleteUser, {
+  userId: session.user.id
+});
+```
+
+## Common Queries
+
+### Get All Sessions for Current User
+```typescript
+import { fetchQuery } from "convex/nextjs";
+import { api } from "@/convex/_generated/api";
+
+const { data: session } = useSession();
+const sessions = await fetchQuery(api.sessions.getByUserId, {
+  userId: session.user.id
+});
+```
+
+### Get All Connected OAuth Accounts
+```typescript
+import { fetchQuery } from "convex/nextjs";
+import { api } from "@/convex/_generated/api";
+
+const { data: session } = useSession();
+const accounts = await fetchQuery(api.accounts.getByUserId, {
+  userId: session.user.id
+});
+
+// Example: ["google", "github"]
+const providers = accounts.map(a => a.provider);
+```
+
+### Check Subscription Status
+```typescript
+import { fetchQuery } from "convex/nextjs";
+import { api } from "@/convex/_generated/api";
+
+const { data: session } = useSession();
+const subscriptionStatus = await fetchQuery(api.users.getSubscriptionStatus, {
+  userId: session.user.id
+});
+
+console.log(subscriptionStatus.plan); // "free" | "pro"
+```
+
+## Configuration
+
+### Session Expiration
+In `src/lib/auth.ts`:
+```typescript
+session: {
+  expiresIn: 7 * 24 * 60 * 60,  // 7 days (change this)
+  updateAge: 24 * 60 * 60,      // Refresh after 1 day
+}
+```
+
+### Disable OAuth Provider
+In `src/lib/auth.ts`:
+```typescript
+socialProviders: {
+  google: {
+    enabled: false  // ← Disable
+  }
+}
+```
+
+### Enable Email Verification
+In `src/lib/auth.ts`:
+```typescript
+emailAndPassword: {
+  enabled: true,
+  requireEmailVerification: true  // ← Change to true
+}
+```
+
+### Change Cookie Prefix
+In `src/lib/auth.ts`:
+```typescript
+advanced: {
+  cookiePrefix: "myapp."  // Cookie names: myapp.session_token, etc.
+}
+```
+
+## Environment Variables
+
+Create `.env.local`:
+```bash
+# Required
+NEXT_PUBLIC_CONVEX_URL=https://xyz.convex.cloud
+NEXT_PUBLIC_APP_URL=http://localhost:3000
+BETTER_AUTH_SECRET=<generate with: openssl rand -base64 32>
+
+# OAuth (Optional)
+GOOGLE_CLIENT_ID=...
+GOOGLE_CLIENT_SECRET=...
+GITHUB_CLIENT_ID=...
+GITHUB_CLIENT_SECRET=...
+
+# Billing
+POLAR_ACCESS_TOKEN=...
+POLAR_WEBHOOK_SECRET=...
+```
+
+## Debugging
+
+### Check Session in Browser Console
+```javascript
+// Get session token from cookie
+document.cookie.split(';').find(c => c.includes('session_token'))
+```
+
+### View Convex Data
+1. Open [Convex Dashboard](https://dashboard.convex.dev)
+2. Select project
+3. View `users`, `sessions`, `accounts` tables
+
+### Check API Responses
+```typescript
+// In browser console while testing
+await fetch("/api/auth/session").then(r => r.json()).then(console.log)
+```
+
+### View OAuth Token Expiration
+```typescript
+const { data: session } = useSession();
+const accounts = await fetchQuery(api.accounts.getByUserId, {
+  userId: session.user.id
+});
+
+accounts.forEach(acc => {
+  const isExpired = acc.expiresAt < Date.now();
+  console.log(`${acc.provider}: ${isExpired ? "EXPIRED" : "VALID"}`);
+});
+```
+
+## Error Handling
+
+### Graceful Fallback
+```typescript
+const { data: session, error } = useSession();
+
+if (error) {
+  console.error("Failed to fetch session:", error);
+  return <div>Unable to load user session</div>;
+}
+
+if (!session) {
+  return <div>Please log in</div>;
+}
+```
+
+### Check OAuth Token Validity
+```typescript
+import { isOAuthTokenExpired } from "@/lib/oauth-token-refresh";
+
+const account = // ... fetch from Convex
+if (isOAuthTokenExpired(account.expiresAt)) {
+  // Token is expired - attempt refresh or prompt user to re-authenticate
+}
+```
+
+## Testing Checklist
+
+- [ ] Email/password sign-up works
+- [ ] Email/password sign-in works
+- [ ] Session persists on page reload
+- [ ] Logout clears session
+- [ ] Google OAuth sign-in works
+- [ ] GitHub OAuth sign-in works
+- [ ] OAuth tokens refresh automatically
+- [ ] Account deletion removes all related data
+- [ ] Rate limiting blocks brute force attempts
+- [ ] User profile can be updated
+- [ ] OAuth providers can be disconnected
+
+## Performance Tips
+
+1. **Cache session in React Query**: Already done via `useSession()`
+2. **Use indexes**: Queries already use `.withIndex()` for common lookups
+3. **Batch operations**: Use `Promise.all()` for parallel Convex calls
+4. **Avoid N+1 queries**: Load related data in single query
+5. **Clean up expired sessions**: Run `sessions.cleanupExpired()` periodically
+
+## Migration from Other Auth Systems
+
+If migrating from Clerk, Auth0, or Supabase:
+
+1. **Export existing users** to CSV/JSON
+2. **Hash passwords** with bcrypt (if needed)
+3. **Create migration script** in `convex/`
+4. **Map user IDs** to Convex IDs (string format: "hashXXX")
+5. **Update all references** in database
+6. **Test thoroughly** before production
+
+See `explanations/MIGRATION_CONVEX_FROM_POSTGRESQL.md` for example.

File: explanations/CORS_FIX_SUMMARY.md
Changes:
@@ -0,0 +1,222 @@
+# CORS Fix Summary
+
+## Problem
+The application at `zapdev.link` was experiencing CORS errors when trying to make API requests to the authentication endpoints:
+
+```
+Access to fetch at 'https://zapdev-mu.vercel.app/api/auth/get-session' 
+from origin 'https://www.zapdev.link' has been blocked by CORS policy
+```
+
+**Root Cause**: The server-side auth configuration had a hardcoded URL pointing to `zapdev-mu.vercel.app` instead of using the actual deployment domain.
+
+## Solution Implemented
+
+### 1. Fixed Server-Side Auth Configuration
+**File**: `src/lib/auth.ts`
+
+**Before**:
+```typescript
+const getBaseURL = (): string => {
+  if (process.env.NODE_ENV === "development") {
+    return "http://localhost:3000";
+  }
+  return "https://zapdev-mu.vercel.app"; // ❌ Hardcoded
+};
+```
+
+**After**:
+```typescript
+const getBaseURL = (): string => {
+  // Use environment variable first (production/staging)
+  if (process.env.BETTER_AUTH_URL) {
+    return process.env.BETTER_AUTH_URL;
+  }
+  // Development fallback
+  if (process.env.NODE_ENV === "development") {
+    return "http://localhost:3000";
+  }
+  // Last resort: use NEXT_PUBLIC_APP_URL
+  return process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";
+};
+```
+
+### 2. Added CORS Headers to Auth API Routes
+**File**: `src/app/api/auth/[...all]/route.ts`
+
+Added:
+- `getAllowedOrigin()` function to validate and allow requests from the app's domain
+- `addCorsHeaders()` function to add proper CORS headers to all responses
+- `OPTIONS` handler for CORS preflight requests
+- CORS header support for both `GET` and `POST` handlers
+
+Features:
+- ✅ Supports both `www.zapdev.link` and `zapdev.link`
+- ✅ Allows credentials (cookies) to be sent with requests
+- ✅ Handles preflight OPTIONS requests
+- ✅ Validates origin against environment variables
+- ✅ Development-friendly (allows localhost in dev mode)
+
+### 3. Updated Next.js Global Headers
+**File**: `next.config.mjs`
+
+Added CORS headers specifically for auth API routes:
+```javascript
+{
+  source: '/api/auth/:path*',
+  headers: [
+    {
+      key: 'Access-Control-Allow-Credentials',
+      value: 'true'
+    },
+    {
+      key: 'Access-Control-Allow-Methods',
+      value: 'GET, POST, PUT, DELETE, OPTIONS'
+    },
+    {
+      key: 'Access-Control-Allow-Headers',
+      value: 'Content-Type, Authorization, Cookie'
+    },
+  ]
+}
+```
+
+### 4. Updated Documentation
+**Files Updated**:
+- `env.example` - Added important note about `BETTER_AUTH_URL`
+- `explanations/DOMAIN_CONFIGURATION_GUIDE.md` - Complete setup guide (NEW)
+- `explanations/CORS_FIX_SUMMARY.md` - This file (NEW)
+
+## What You Need to Do
+
+### Step 1: Set Environment Variables in Vercel
+
+Go to your Vercel project settings and add/update:
+
+```bash
+BETTER_AUTH_URL=https://zapdev.link
+NEXT_PUBLIC_APP_URL=https://zapdev.link
+```
+
+**Important**: 
+- Use your actual domain (not `zapdev-mu.vercel.app`)
+- Include `https://` protocol
+- No trailing slashes
+- Both variables should match
+
+### Step 2: Redeploy
+
+After setting the environment variables, trigger a new deployment:
+
+```bash
+git add .
+git commit -m "Fix CORS issues with dynamic domain configuration"
+git push
+```
+
+Or trigger a redeploy in Vercel dashboard.
+
+### Step 3: Update OAuth Providers (if using)
+
+If you're using Google or GitHub OAuth, update their redirect URIs:
+
+**Google OAuth**:
+- Authorized redirect URIs: `https://zapdev.link/api/auth/callback/google`
+- Authorized JavaScript origins: `https://zapdev.link`
+
+**GitHub OAuth**:
+- Authorization callback URL: `https://zapdev.link/api/auth/callback/github`
+- Homepage URL: `https://zapdev.link`
+
+### Step 4: Update Polar.sh Webhook (if using)
+
+Update your Polar.sh webhook URL to:
+```
+https://zapdev.link/api/polar/webhooks
+```
+
+### Step 5: Test
+
+1. Clear browser cache and cookies
+2. Visit `https://zapdev.link`
+3. Try signing in
+4. Check browser console - no CORS errors should appear
+5. All API requests should go to `zapdev.link`, not `zapdev-mu.vercel.app`
+
+## How It Works
+
+### Development (localhost)
+```
+Client (localhost:3000) → Auth API (localhost:3000)
+✅ Same origin, no CORS needed
+```
+
+### Production (before fix)
+```
+Client (zapdev.link) → Auth API (zapdev-mu.vercel.app)
+❌ Different origins, CORS error
+```
+
+### Production (after fix)
+```
+Client (zapdev.link) → Auth API (zapdev.link)
+✅ Same origin, CORS headers present
+```
+
+## Technical Details
+
+### CORS Headers Added
+- `Access-Control-Allow-Origin`: Dynamically set to match request origin
+- `Access-Control-Allow-Credentials`: `true` (allows cookies)
+- `Access-Control-Allow-Methods`: `GET, POST, PUT, DELETE, OPTIONS`
+- `Access-Control-Allow-Headers`: `Content-Type, Authorization, Cookie`
+- `Access-Control-Max-Age`: `86400` (24 hours cache for preflight)
+
+### Security Features
+- Origin validation against environment variables
+- Support for both www and non-www subdomains
+- Development mode allows localhost
+- Credentials (cookies) only allowed for validated origins
+
+## Troubleshooting
+
+If you still see CORS errors after deploying:
+
+1. **Check environment variables are set in Vercel**
+   - Go to Settings → Environment Variables
+   - Verify `BETTER_AUTH_URL` and `NEXT_PUBLIC_APP_URL` are set correctly
+
+2. **Hard refresh your browser**
+   - Chrome/Firefox: `Ctrl+Shift+R` (Windows) or `Cmd+Shift+R` (Mac)
+   - Or try incognito/private mode
+
+3. **Verify deployment used new environment variables**
+   - Redeploy if environment variables were added after last deployment
+   - Check deployment logs for any errors
+
+4. **Check for www vs non-www mismatch**
+   - Ensure you're accessing the same domain set in environment variables
+   - Consider setting up a redirect from one to the other in Vercel
+
+5. **Clear all cookies for your domain**
+   - Old session cookies might cause issues
+   - Sign out completely and sign in again
+
+## Files Changed
+
+- ✅ `src/lib/auth.ts` - Dynamic base URL configuration
+- ✅ `src/app/api/auth/[...all]/route.ts` - CORS headers and OPTIONS handler
+- ✅ `next.config.mjs` - Global CORS headers for auth routes
+- ✅ `env.example` - Updated documentation
+- ✅ `explanations/DOMAIN_CONFIGURATION_GUIDE.md` - Complete setup guide
+- ✅ `explanations/CORS_FIX_SUMMARY.md` - This summary
+
+## Next Steps
+
+1. Deploy the changes to production
+2. Set the required environment variables
+3. Test authentication flows
+4. Update OAuth provider settings
+5. Monitor for any remaining CORS issues
+
+For detailed setup instructions, see: `explanations/DOMAIN_CONFIGURATION_GUIDE.md`

File: explanations/DOMAIN_CONFIGURATION_GUIDE.md
Changes:
@@ -0,0 +1,218 @@
+# Domain Configuration Guide
+
+This guide will help you properly configure ZapDev to work with your custom domain and fix CORS issues.
+
+## The Problem
+
+CORS errors occur when the application's frontend and backend use different domains. For example:
+- Frontend served from: `https://zapdev.link`
+- Backend API calls going to: `https://zapdev-mu.vercel.app`
+
+This causes browser security to block the requests with CORS policy errors.
+
+## The Solution
+
+### 1. Configure Environment Variables
+
+Set these environment variables in your production deployment (Vercel, etc.):
+
+```bash
+# Your actual domain (must match what users see in their browser)
+BETTER_AUTH_URL=https://zapdev.link
+NEXT_PUBLIC_APP_URL=https://zapdev.link
+```
+
+**Important Notes:**
+- Use `https://` for production (not `http://`)
+- Don't include trailing slashes
+- Both variables should have the same value
+- If using `www.zapdev.link`, set it to that instead
+
+### 2. Vercel Configuration Steps
+
+If deploying to Vercel:
+
+1. Go to your project settings: https://vercel.com/[your-username]/[project-name]/settings/environment-variables
+
+2. Add/Update these variables:
+   - `BETTER_AUTH_URL` → `https://zapdev.link`
+   - `NEXT_PUBLIC_APP_URL` → `https://zapdev.link`
+
+3. **Important**: Redeploy after changing environment variables
+   ```bash
+   # Trigger a new deployment
+   git commit --allow-empty -m "Trigger redeploy for env vars"
+   git push
+   ```
+
+### 3. Domain Setup
+
+#### Option A: Use Root Domain Only (Recommended)
+- Primary domain: `zapdev.link`
+- Redirect `www.zapdev.link` → `zapdev.link`
+
+In Vercel:
+1. Go to Settings → Domains
+2. Add `zapdev.link` as primary
+3. Add `www.zapdev.link` and set it to redirect to `zapdev.link`
+
+Set environment variables:
+```bash
+BETTER_AUTH_URL=https://zapdev.link
+NEXT_PUBLIC_APP_URL=https://zapdev.link
+```
+
+#### Option B: Use WWW Subdomain
+- Primary domain: `www.zapdev.link`
+- Redirect `zapdev.link` → `www.zapdev.link`
+
+Set environment variables:
+```bash
+BETTER_AUTH_URL=https://www.zapdev.link
+NEXT_PUBLIC_APP_URL=https://www.zapdev.link
+```
+
+### 4. OAuth Provider Updates
+
+After changing your domain, update OAuth redirect URIs:
+
+#### Google OAuth
+1. Go to [Google Cloud Console](https://console.cloud.google.com/)
+2. Navigate to: APIs & Services → Credentials
+3. Edit your OAuth 2.0 Client ID
+4. Update Authorized redirect URIs:
+   ```
+   https://zapdev.link/api/auth/callback/google
+   ```
+5. Update Authorized JavaScript origins:
+   ```
+   https://zapdev.link
+   ```
+
+#### GitHub OAuth
+1. Go to [GitHub Settings](https://github.com/settings/developers)
+2. Edit your OAuth App
+3. Update Authorization callback URL:
+   ```
+   https://zapdev.link/api/auth/callback/github
+   ```
+4. Update Homepage URL:
+   ```
+   https://zapdev.link
+   ```
+
+### 5. Polar.sh Webhook URL
+
+Update your Polar.sh webhook endpoint:
+
+1. Go to [Polar.sh Dashboard](https://polar.sh/)
+2. Navigate to Settings → Webhooks
+3. Update webhook URL to:
+   ```
+   https://zapdev.link/api/polar/webhooks
+   ```
+
+## Testing the Fix
+
+### 1. Check Environment Variables
+After deployment, verify variables are set correctly:
+
+```bash
+# In your browser console on https://zapdev.link
+console.log(window.location.origin);  // Should show https://zapdev.link
+```
+
+### 2. Test Authentication
+1. Clear browser cookies and cache
+2. Go to your domain: `https://zapdev.link`
+3. Try signing in with email/password
+4. Check browser console for any CORS errors
+5. Try OAuth sign-in (Google/GitHub)
+
+### 3. Verify API Requests
+In browser DevTools Network tab:
+- All API requests should go to `https://zapdev.link/api/*`
+- No requests should go to `zapdev-mu.vercel.app`
+- Response headers should include `Access-Control-Allow-Origin: https://zapdev.link`
+
+## Common Issues
+
+### Issue: Still seeing old domain in requests
+
+**Solution**: 
+- Hard refresh: `Ctrl+Shift+R` (Windows/Linux) or `Cmd+Shift+R` (Mac)
+- Clear browser cache completely
+- Try incognito/private window
+- Verify environment variables in Vercel dashboard
+
+### Issue: OAuth redirects to wrong domain
+
+**Solution**:
+- Double-check OAuth provider settings match your domain exactly
+- Ensure no typos in redirect URIs
+- Wait a few minutes for OAuth provider changes to propagate
+
+### Issue: CORS errors persist
+
+**Solution**:
+1. Verify both `BETTER_AUTH_URL` and `NEXT_PUBLIC_APP_URL` are identical
+2. Check they match your actual domain (no www mismatch)
+3. Ensure you redeployed after setting environment variables
+4. Clear browser cache and cookies
+
+### Issue: Session cookies not working
+
+**Solution**:
+- Check that your domain supports secure cookies (HTTPS)
+- Verify `BETTER_AUTH_SECRET` is set in production
+- Clear all cookies for your domain
+- Try signing in again
+
+## Development vs Production
+
+### Development (localhost:3000)
+```bash
+BETTER_AUTH_URL=http://localhost:3000
+NEXT_PUBLIC_APP_URL=http://localhost:3000
+```
+
+### Production (zapdev.link)
+```bash
+BETTER_AUTH_URL=https://zapdev.link
+NEXT_PUBLIC_APP_URL=https://zapdev.link
+```
+
+## Verification Checklist
+
+Before going live, verify:
+
+- [ ] `BETTER_AUTH_URL` environment variable set in production
+- [ ] `NEXT_PUBLIC_APP_URL` environment variable set in production
+- [ ] Both variables use the same domain
+- [ ] Domain uses HTTPS (not HTTP)
+- [ ] No trailing slashes in URLs
+- [ ] Vercel domain settings configured correctly
+- [ ] OAuth providers updated with new redirect URIs
+- [ ] Polar.sh webhook URL updated
+- [ ] Tested sign-in flow (email + OAuth)
+- [ ] No CORS errors in browser console
+- [ ] All API requests go to correct domain
+
+## Need Help?
+
+If you're still experiencing issues:
+
+1. Check browser console for specific error messages
+2. Review Vercel deployment logs
+3. Verify all environment variables are set correctly
+4. Test in incognito mode to rule out caching issues
+
+## Technical Details
+
+The CORS fix implemented includes:
+
+1. **Dynamic base URL** (`src/lib/auth.ts`): Uses environment variables instead of hardcoded domain
+2. **CORS headers** (`src/app/api/auth/[...all]/route.ts`): Adds proper CORS headers to all auth endpoints
+3. **Preflight support**: Handles OPTIONS requests for CORS preflight checks
+4. **WWW subdomain support**: Automatically allows both www and non-www variants
+5. **Next.js headers** (`next.config.mjs`): Global CORS headers for API routes

File: explanations/SCHEMA_VALIDATION_FIX.md
Changes:
@@ -0,0 +1,207 @@
+# Schema Validation Error Fix: Orphaned Projects Cleanup
+
+## Problem
+
+You encountered this error:
+```
+Schema validation failed.
+Document with ID "jn7034wkypcnpdyvkn0vd7r8n57tnrhf" in table "projects" does not match the schema: Value does not match validator.
+Path: .userId
+Value: "user_30xqHm23FRYopIgyfPPYsnMGqAq"
+Validator: v.id("users")
+```
+
+This error occurs when the `projects` table contains documents with `userId` values that don't reference valid user IDs in the `users` table. This typically happens after migrating from Clerk to Better Auth, where old user IDs may not be properly linked to the new user records.
+
+## Root Cause
+
+During the Clerk → Better Auth migration, some projects in the database may still reference user IDs from the old authentication system. Convex's schema enforces referential integrity by default, which means:
+
+1. **Project documents** have a `userId` field defined as `v.id("users")`
+2. **Convex validates** that every referenced ID actually exists in the referenced table
+3. **Orphaned documents** with invalid references fail validation
+
+## Solution
+
+We've created a cleanup utility that:
+1. ✅ Identifies all projects with invalid `userId` references
+2. ✅ Cascades deletion of all related data (messages, fragments, attachments)
+3. ✅ Removes the orphaned project documents
+4. ✅ Restores database integrity
+
+## How to Run the Cleanup
+
+### Prerequisites
+
+1. **Start Convex Dev Server** (in a separate terminal)
+   ```bash
+   bun run convex:dev
+   ```
+   Keep this running! The cleanup script needs it.
+
+2. **Set Environment Variables**
+   After starting `convex:dev`, you'll see output like:
+   ```
+   ✓ Convex dev server is running!
+   To open the dashboard, run:
+   > npx convex dashboard --url <YOUR_URL>
+   ```
+   
+   Copy the URL and add to your `.env`:
+   ```bash
+   NEXT_PUBLIC_CONVEX_URL=<YOUR_URL>
+   ```
+
+### Run the Cleanup
+
+In a new terminal:
+```bash
+bun scripts/cleanup-orphaned-projects.ts
+```
+
+You'll see output like:
+```
+🚀 Initializing Convex client...
+   URL: https://your-deployment.convex.cloud
+
+🧹 Starting cleanup of orphaned projects...
+
+✅ Cleaned up 1 orphaned projects and related data
+   Cleaned projects: 1
+
+📋 Removed project IDs:
+   - jn7034wkypcnpdyvkn0vd7r8n57tnrhf
+
+✅ Cleanup completed successfully!
+```
+
+## What Gets Deleted
+
+The cleanup uses a cascade deletion approach to maintain data integrity:
+
+```
+Orphaned Project
+├── Messages
+│   ├── Attachments (deleted)
+│   ├── Fragments (deleted)
+│   └── Message itself (deleted)
+├── Fragment Drafts (deleted)
+└── Project itself (deleted)
+```
+
+**Important**: Only projects with invalid `userId` references are removed. All valid projects remain untouched.
+
+## Verification
+
+After running the cleanup, you can verify the fix:
+
+### 1. Check Dashboard
+```bash
+bun run convex:dev
+# Then visit: https://dashboard.convex.dev
+```
+
+Navigate to the **Data** tab and verify:
+- Projects with valid `userId` references remain
+- No documents remain with orphaned references
+
+### 2. Run the App
+```bash
+# Terminal 1: Convex dev server
+bun run convex:dev
+
+# Terminal 2: Next.js dev server
+bun run dev
+```
+
+The app should now work without schema validation errors.
+
+### 3. Test Project Creation
+1. Sign up or sign in at http://localhost:3000
+2. Create a new project
+3. Verify it saves correctly to the database
+
+## Code Changes
+
+### Files Modified
+- `convex/importData.ts` - Added `cleanupOrphanedProjects` mutation and action
+
+### New Functions
+
+**`cleanupOrphanedProjects` (internal mutation)**
+- Finds all projects with invalid `userId` references
+- Cascades deletion of related data
+- Returns count of cleaned projects
+
+**`cleanupOrphanedProjectsAction` (public action)**
+- HTTP-accessible wrapper for the cleanup function
+- Used by the cleanup script
+
+**`scripts/cleanup-orphaned-projects.ts` (admin script)**
+- CLI tool to run the cleanup from the command line
+- Provides user-friendly output
+
+## When to Run This
+
+Run this cleanup when you see the schema validation error. It's safe to run multiple times:
+
+- ✅ Safe to run multiple times (idempotent)
+- ✅ Only removes orphaned documents
+- ✅ Won't affect valid user projects
+- ✅ Can be run during development or production
+
+## Troubleshooting
+
+### Error: "NEXT_PUBLIC_CONVEX_URL is not set"
+**Solution**: 
+1. Run `bun run convex:dev` first
+2. Copy the URL from the output
+3. Add to `.env`: `NEXT_PUBLIC_CONVEX_URL=<URL>`
+
+### Error: "Cannot connect to Convex"
+**Solution**:
+1. Make sure `bun run convex:dev` is still running
+2. Verify the URL in `.env` matches the dev server output
+3. Check internet connection
+
+### Cleanup shows 0 cleaned projects but error still occurs
+**Possible causes**:
+1. The orphaned data was already removed
+2. The validation error is from a different table (check the error message)
+3. New data was inserted after you ran the cleanup
+
+**Next steps**: Check the Convex dashboard directly to find the problem data.
+
+## Additional Notes
+
+### Data Safety
+- ✅ Original data is not modified before deletion
+- ✅ No data is moved or transformed
+- ✅ Only orphaned documents are removed
+- ✅ Can be re-run if needed
+
+### Performance
+- For small databases: < 1 second
+- For large databases: up to a few seconds depending on orphaned count
+
+### Future Prevention
+To prevent this issue in the future:
+
+1. **During migration**: Ensure all user references are updated to valid Convex user IDs
+2. **In validation**: Test that all `userId` references exist before creating projects
+3. **In imports**: Always validate foreign key references before inserting data
+
+## Related Documentation
+
+- [BETTER_AUTH_POLAR_SETUP.md](./BETTER_AUTH_POLAR_SETUP.md) - Setup guide for Better Auth
+- [DATA_MIGRATION_GUIDE.md](./DATA_MIGRATION_GUIDE.md) - PostgreSQL to Convex migration
+- [MIGRATION_CLERK_TO_BETTER_AUTH.md](../MIGRATION_CLERK_TO_BETTER_AUTH.md) - Clerk to Better Auth migration
+
+## Questions?
+
+If you encounter issues with the cleanup:
+
+1. Check the error message carefully - it indicates what went wrong
+2. Review the troubleshooting section above
+3. Check Convex logs: `bun run convex logs`
+4. Verify your Convex deployment status

File: next.config.mjs
Changes:
@@ -24,6 +24,23 @@ const nextConfig = {
   /* config options here */
   headers: async () => {
     return [
+      {
+        source: '/api/auth/:path*',
+        headers: [
+          {
+            key: 'Access-Control-Allow-Credentials',
+            value: 'true'
+          },
+          {
+            key: 'Access-Control-Allow-Methods',
+            value: 'GET, POST, PUT, DELETE, OPTIONS'
+          },
+          {
+            key: 'Access-Control-Allow-Headers',
+            value: 'Content-Type, Authorization, Cookie'
+          },
+        ]
+      },
       {
         source: '/:path*',
         headers: [

File: scripts/cleanup-orphaned-projects.ts
Changes:
@@ -0,0 +1,66 @@
+#!/usr/bin/env bun
+/**
+ * Admin script to clean up orphaned projects from Convex database
+ * 
+ * This script removes all projects that have userId references pointing to
+ * non-existent users in the users table. This fixes schema validation errors
+ * that occur when the database has stale references.
+ * 
+ * Usage:
+ *   bun scripts/cleanup-orphaned-projects.ts
+ * 
+ * Prerequisites:
+ *   1. Run `bun run convex:dev` in a separate terminal
+ *   2. Set NEXT_PUBLIC_CONVEX_URL in .env from the convex dev server output
+ */
+
+import { ConvexClient } from "convex/browser";
+import { api } from "../convex/_generated/api";
+
+async function cleanupOrphanedProjects() {
+  const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL;
+
+  if (!convexUrl) {
+    console.error("❌ Error: NEXT_PUBLIC_CONVEX_URL is not set");
+    console.error("   Please ensure you've run `bun run convex:dev` and set the URL in .env");
+    process.exit(1);
+  }
+
+  console.log("🚀 Initializing Convex client...");
+  console.log(`   URL: ${convexUrl}`);
+
+  const client = new ConvexClient(convexUrl);
+
+  try {
+    console.log("\n🧹 Starting cleanup of orphaned projects...\n");
+
+    const result = await client.action(api.importData.cleanupOrphanedProjectsAction, {});
+
+    console.log(`✅ ${result.message}`);
+    console.log(`   Total cleaned: ${result.totalCleaned}`);
+    console.log(`   - Projects: ${result.cleanedProjectCount}`);
+    console.log(`   - Usage records: ${result.cleanedUsageCount}`);
+    console.log(`   - OAuth connections: ${result.cleanedOAuthCount}`);
+    console.log(`   - Imports: ${result.cleanedImportsCount}`);
+
+    if (result.orphanedProjectIds.length > 0) {
+      console.log(`\n📋 Removed ${result.orphanedProjectIds.length} orphaned project IDs (showing first 10):`);
+      for (const id of result.orphanedProjectIds.slice(0, 10)) {
+        console.log(`   - ${id}`);
+      }
+      if (result.orphanedProjectIds.length > 10) {
+        console.log(`   ... and ${result.orphanedProjectIds.length - 10} more`);
+      }
+    } else {
+      console.log("\n✨ No orphaned data found!");
+    }
+
+    console.log("\n✅ Cleanup completed successfully!");
+    process.exit(0);
+  } catch (error) {
+    console.error("❌ Error during cleanup:", error);
+    process.exit(1);
+  }
+}
+
+cleanupOrphanedProjects();

File: src/app/api/auth/[...all]/route.ts
Changes:
@@ -11,38 +11,104 @@ function getHandlers() {
   return handlers;
 }
 
-// Wrap POST handler with rate limiting
+// Get allowed origins for CORS
+function getAllowedOrigin(request: Request): string {
+  const origin = request.headers.get("origin");
+  const appUrl = process.env.NEXT_PUBLIC_APP_URL || process.env.BETTER_AUTH_URL || "http://localhost:3000";
+  
+  // Allow requests from the app's own domain
+  if (origin && (origin === appUrl || origin === `${appUrl}`.replace(/\/$/, ""))) {
+    return origin;
+  }
+  
+  // Also allow www subdomain variant
+  const wwwVariant = appUrl.replace("://", "://www.");
+  const nonWwwVariant = appUrl.replace("://www.", "://");
+  
+  if (origin && (origin === wwwVariant || origin === nonWwwVariant)) {
+    return origin;
+  }
+  
+  // For development, allow localhost
+  if (process.env.NODE_ENV === "development" && origin?.includes("localhost")) {
+    return origin;
+  }
+  
+  // Default to app URL
+  return appUrl;
+}
+
+// Add CORS headers to response
+function addCorsHeaders(response: Response, origin: string): Response {
+  const headers = new Headers(response.headers);
+  headers.set("Access-Control-Allow-Origin", origin);
+  headers.set("Access-Control-Allow-Credentials", "true");
+  headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
+  headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization, Cookie");
+  
+  return new Response(response.body, {
+    status: response.status,
+    statusText: response.statusText,
+    headers,
+  });
+}
+
+// Handle preflight OPTIONS requests for CORS
+export async function OPTIONS(request: Request) {
+  const origin = getAllowedOrigin(request);
+  
+  return new Response(null, {
+    status: 204,
+    headers: {
+      'Access-Control-Allow-Origin': origin,
+      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
+      'Access-Control-Allow-Headers': 'Content-Type, Authorization, Cookie',
+      'Access-Control-Allow-Credentials': 'true',
+      'Access-Control-Max-Age': '86400', // 24 hours
+    },
+  });
+}
+
+// Wrap POST handler with rate limiting and CORS
 export async function POST(request: Request) {
+  const origin = getAllowedOrigin(request);
+  
   try {
     // Check rate limit before processing auth request
     const rateLimitResult = await checkRateLimit(request);
 
     if (!rateLimitResult.success && rateLimitResult.response) {
-      return rateLimitResult.response;
+      return addCorsHeaders(rateLimitResult.response, origin);
     }
 
     // Continue with original handler
     const h = getHandlers();
-    return h.POST(request);
+    const response = await h.POST(request);
+    return addCorsHeaders(response, origin);
   } catch (error) {
     console.error("Auth POST handler error:", error);
-    return new Response(JSON.stringify({ error: "Authentication failed" }), {
+    const errorResponse = new Response(JSON.stringify({ error: "Authentication failed" }), {
       status: 500,
       headers: { "Content-Type": "application/json" },
     });
+    return addCorsHeaders(errorResponse, origin);
   }
 }
 
-// GET requests don't need rate limiting (mostly for OAuth callbacks)
+// GET requests with CORS support (mostly for OAuth callbacks and session checks)
 export async function GET(request: Request) {
+  const origin = getAllowedOrigin(request);
+  
   try {
     const h = getHandlers();
-    return h.GET(request);
+    const response = await h.GET(request);
+    return addCorsHeaders(response, origin);
   } catch (error) {
     console.error("Auth GET handler error:", error);
-    return new Response(JSON.stringify({ error: "Authentication failed" }), {
+    const errorResponse = new Response(JSON.stringify({ error: "Authentication failed" }), {
       status: 500,
       headers: { "Content-Type": "application/json" },
     });
+    return addCorsHeaders(errorResponse, origin);
   }
 }

File: src/lib/auth.ts
Changes:
@@ -32,11 +32,16 @@ const makeSocialProviders = () => {
 
 // Determine base URL based on environment
 const getBaseURL = (): string => {
+  // Use environment variable first (production/staging)
+  if (process.env.BETTER_AUTH_URL) {
+    return process.env.BETTER_AUTH_URL;
+  }
+  // Development fallback
   if (process.env.NODE_ENV === "development") {
     return "http://localhost:3000";
   }
-  // Production URL - use hardcoded domain
-  return "https://zapdev-mu.vercel.app";
+  // Last resort: use NEXT_PUBLIC_APP_URL
+  return process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";
 };
 
 const baseURL = getBaseURL();

@vercel
Copy link

vercel bot commented Nov 12, 2025

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

Project Deployment Preview Comments Updated (UTC)
zapdev Building Building Preview Comment Nov 12, 2025 1:32am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 12, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

This PR adds orphaned project cleanup functionality to Convex, implements CORS fixes for authentication endpoints, introduces comprehensive Better Auth integration documentation, updates base URL selection logic with environment variable precedence, and provides an admin CLI script for running cleanup operations.

Changes

Cohort / File(s) Summary
Data Cleanup & Schema
convex/importData.ts, convex/schema.ts, scripts/cleanup-orphaned-projects.ts
Adds internal mutation cleanupOrphanedProjects and public action cleanupOrphanedProjectsAction to identify and delete orphaned projects with cascading deletion of related data (messages, attachments, fragments, drafts, usage, OAuth connections, imports). Introduces admin CLI script to invoke cleanup from command line. Minor comment updates to schema field references.
Authentication CORS & Configuration
src/app/api/auth/[...all]/route.ts, src/lib/auth.ts, next.config.mjs
Adds CORS support to auth routes with origin validation, header injection, and OPTIONS preflight handler. Updates base URL selection logic to prioritize BETTER_AUTH_URL environment variable. Configures Next.js global headers for /api/auth routes with CORS credentials and method allowances.
Environment & Documentation
env.example, explanations/BETTER_AUTH_CONVEX_INTEGRATION.md, explanations/BETTER_AUTH_CONVEX_QUICK_REFERENCE.md, explanations/CORS_FIX_SUMMARY.md, explanations/DOMAIN_CONFIGURATION_GUIDE.md, explanations/SCHEMA_VALIDATION_FIX.md
Updates BETTER_AUTH_URL comment in environment template. Adds five comprehensive documentation files covering Better Auth + Convex architecture, integration patterns, CORS implementation details, domain configuration guidance, and schema validation cleanup workflow.

Sequence Diagram(s)

sequenceDiagram
    participant Admin as Admin/Script
    participant Action as cleanupOrphanedProjectsAction
    participant Mutation as cleanupOrphanedProjects (Internal)
    participant DB as Convex DB
    participant Result as Cleanup Result

    Admin->>Action: Invoke action (HTTP)
    Action->>Mutation: ctx.runMutation()
    activate Mutation
    Mutation->>DB: Query all users
    Mutation->>DB: Query all projects
    rect rgb(220, 240, 255)
        note over Mutation: Identify orphaned projects<br/>(userId not in users set)
    end
    Mutation->>DB: Delete orphaned projects
    Mutation->>DB: Delete related messages
    Mutation->>DB: Delete related attachments
    Mutation->>DB: Delete related fragments
    Mutation->>DB: Delete related fragment drafts
    Mutation->>DB: Delete orphaned usage records
    Mutation->>DB: Delete orphaned OAuth connections
    Mutation->>DB: Delete orphaned imports
    rect rgb(240, 255, 240)
        note over Mutation: Return cleanup summary<br/>(counts & orphaned IDs)
    end
    Mutation-->>Action: Return summary
    deactivate Mutation
    Action-->>Admin: Return result with cleanup stats
    Admin->>Result: Display results to user
Loading
sequenceDiagram
    participant Client as Browser Client
    participant Routes as Auth API Routes
    participant Handler as OPTIONS/POST/GET Handler
    participant CORS as CORS Middleware
    participant Backend as Backend Handler

    Client->>Routes: Preflight OPTIONS request
    Routes->>Handler: OPTIONS(request)
    Handler->>CORS: getAllowedOrigin(request)
    CORS-->>Handler: Validated origin
    Handler->>CORS: addCorsHeaders(response, origin)
    Handler-->>Client: 200 OK + CORS headers

    Client->>Routes: POST/GET request
    Routes->>Handler: POST/GET(request)
    Handler->>CORS: getAllowedOrigin(request)
    CORS-->>Handler: Validated origin
    Handler->>Backend: Process auth request
    Backend-->>Handler: Response
    Handler->>CORS: addCorsHeaders(response, origin)
    Handler-->>Client: Response + CORS headers
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Cleanup mutation logic: Verify cascading deletion order (projects → related data) and orphan detection correctness, especially the userId reference validation and data consistency
  • CORS implementation: Confirm origin validation logic handles all cases (www subdomain, localhost in dev, production domain) and preflight responses are RFC-compliant
  • Environment variable precedence in src/lib/auth.ts: Validate the fallback chain (BETTER_AUTH_URL → NODE_ENV development check → NEXT_PUBLIC_APP_URL → default) doesn't introduce unexpected behavior
  • Admin script error handling: Review error exit codes and validation of NEXT_PUBLIC_CONVEX_URL

Possibly related PRs

  • migration #138: Modifies convex/importData.ts with userId type references and import-related logic; related through shared data cleanup context and schema migration concerns
  • feat/figma github imports #130: Also modifies convex/importData.ts and introduces similar Convex mutations/actions for import operations; related through overlapping implementation area and mutation patterns

Suggested reviewers

  • dogesman098

Poem

🐰 Hop, skip, and cleanup we go,
Orphaned projects? Watch them go!
CORS headers dancing in place,
Auth routes now embrace!
Clean schemas, docs so bright,
ZapDev's configured just right!

✨ 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 migration-better-auth-polar-sh

📜 Recent 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 a5037fe and 0b8c724.

📒 Files selected for processing (12)
  • convex/importData.ts (4 hunks)
  • convex/schema.ts (4 hunks)
  • env.example (1 hunks)
  • explanations/BETTER_AUTH_CONVEX_INTEGRATION.md (1 hunks)
  • explanations/BETTER_AUTH_CONVEX_QUICK_REFERENCE.md (1 hunks)
  • explanations/CORS_FIX_SUMMARY.md (1 hunks)
  • explanations/DOMAIN_CONFIGURATION_GUIDE.md (1 hunks)
  • explanations/SCHEMA_VALIDATION_FIX.md (1 hunks)
  • next.config.mjs (1 hunks)
  • scripts/cleanup-orphaned-projects.ts (1 hunks)
  • src/app/api/auth/[...all]/route.ts (1 hunks)
  • src/lib/auth.ts (1 hunks)

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.

@Jackson57279 Jackson57279 merged commit cf9a93c into master Nov 12, 2025
10 of 13 checks passed
@Jackson57279 Jackson57279 deleted the migration-better-auth-polar-sh branch November 12, 2025 01:32
@codecapyai
Copy link

codecapyai bot commented Nov 12, 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:30011/env'\nFor more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400"}

@codecapyai
Copy link

codecapyai bot commented Nov 12, 2025

🔧 Setting up test environment...

Agent Steps

@codecapyai
Copy link

codecapyai bot commented Nov 12, 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_011CV38mxcdE5QhYz6Dj79Ja'}", 'provider': 'anthropic', 'error_type': 'ProviderAPIError'}}


## API Routes

### src/app/api/auth/[...all]/route.ts

Check notice

Code scanning / Remark-lint (reported by Codacy)

Warn when references to undefined definitions are found. Note

[no-undefined-references] Found reference to undefined definition

## API Routes

### src/app/api/auth/[...all]/route.ts

Check notice

Code scanning / Remark-lint (reported by Codacy)

Warn when shortcut reference links are used. Note

[no-shortcut-reference-link] Use the trailing [] on reference links

If deploying to Vercel:

1. Go to your project settings: https://vercel.com/[your-username]/[project-name]/settings/environment-variables

Check notice

Code scanning / Remark-lint (reported by Codacy)

Warn for literal URLs in text. Note

[no-literal-urls] Don’t use literal URLs without angle brackets
@claude
Copy link

claude bot commented Nov 12, 2025

Pull Request Review: Better Auth Migration & Schema Validation Fixes

This PR addresses schema validation issues from the Clerk to Better Auth migration by implementing a comprehensive cleanup utility for orphaned data.

Critical Issues Found

1. Security: Missing Access Control

The cleanupOrphanedProjectsAction is a public action with no authentication (convex/importData.ts:512-523). Anyone can trigger mass deletion of data. This needs authentication/authorization before merging.

2. Performance: O(n²) Complexity

The cleanup function loads ALL records into memory using .collect() and uses nested loops (convex/importData.ts:285-374). At scale (1000+ projects), this could cause memory issues and timeouts. Recommend using indexed queries instead.

3. Type Safety: Excessive as any Casting

Throughout the cleanup function (lines 286, 297-298, 307, 337, 345, 352, 361, 370), there are 10+ instances of as any. This defeats TypeScript's purpose and could hide type mismatches.

Other Issues

Medium Priority:

  • CORS config missing Access-Control-Allow-Origin header (next.config.mjs:28-42)
  • No unit tests for the cleanup logic
  • Inconsistent comment style in schema files

Minor:

  • CLI script lacks connection validation before running cleanup

Strengths

✅ Well-structured cascade deletion logic
✅ Excellent documentation (667+ lines covering architecture and integration)
✅ Includes convenient CLI tool for admin tasks

Recommendation

Request changes for security and performance fixes before merging to production. The cleanup utility needs hardening for scale and security.

See detailed review with code examples and recommendations in next comment.

@claude
Copy link

claude bot commented Nov 12, 2025

Detailed Recommendations

Fix 1: Add Authentication to Cleanup Action

The cleanupOrphanedProjectsAction needs authentication. Currently anyone can trigger mass deletion.

Recommended fix in convex/importData.ts:512-523:

  • Add adminToken argument
  • Verify against environment variable
  • Log all cleanup operations

Fix 2: Optimize Performance with Indexed Queries

Current approach loads ALL projects/usage/oauth/imports into memory which will not scale.

Recommendation:

  • Use by_userId indexes to query per-user data
  • Avoid nested loops over all records
  • Consider implementing as scheduled Inngest job for large datasets

Fix 3: Remove Type Safety Issues

10+ instances of "as any" casting defeat TypeScript benefits.

Fix: Use proper Id<"users"> type for validUserIds Set instead of string.

Fix 4: Add CORS Origin Control

Auth endpoints missing Access-Control-Allow-Origin header in next.config.mjs:28-42.

Add explicit origin: process.env.NEXT_PUBLIC_APP_URL

Fix 5: Add Test Coverage

No tests for cleanup logic. Add tests to verify:

  • Orphaned data detection accuracy
  • Valid data preservation
  • Cascade deletion correctness

What Works Well

✅ Cascade deletion properly ordered (attachments -> fragments -> messages -> projects)
✅ Documentation is comprehensive with architecture diagrams
✅ CLI script provides good UX with clear output
✅ Returns detailed statistics about cleanup operations

Overall: Great migration work, but needs security and performance hardening before production.

@claude
Copy link

claude bot commented Nov 12, 2025

Review Summary

Status: ⚠️ Changes Requested

Critical Issues: 2

  1. Security: Public cleanup action needs authentication
  2. Performance: O(n²) complexity will not scale

Medium Issues: 3

  1. Type safety compromised with excessive "as any"
  2. CORS configuration incomplete
  3. No test coverage

Positive: Excellent documentation, well-structured logic, solves the orphaned data problem

Next Steps:

  1. Add authentication to cleanupOrphanedProjectsAction
  2. Optimize queries to use indexes instead of .collect() on all tables
  3. Remove type casts and fix type definitions
  4. Add unit tests for cleanup logic
  5. Complete CORS configuration

The architectural documentation (BETTER_AUTH_CONVEX_INTEGRATION.md) is exceptional and will be very helpful for maintenance. With the security and performance fixes, this will be a solid addition to the codebase.

Note: PR is already merged, so these issues should be addressed in a follow-up PR.

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