Conversation
- 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
CodeCapy Review ₍ᐢ•(ܫ)•ᐢ₎
Codebase SummaryThis 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 ChangesThe pull request implements multiple changes including:
Setup Instructions
Generated Test Cases1: 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:
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:
Steps:
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:
Steps:
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:
Steps:
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:
Steps:
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:
Steps:
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:
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 AnalyzedFile: 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();
|
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Caution Review failedThe pull request is closed. WalkthroughThis 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
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
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes
Possibly related PRs
Suggested reviewers
Poem
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro Disabled knowledge base sources:
📒 Files selected for processing (12)
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. Comment |
|
🚀 Scrapybara Ubuntu instance started! |
|
🔧 Setting up test environment... Agent Steps |
|
❌ Something went wrong: |
|
|
||
| ## 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
|
|
||
| ## 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
|
|
||
| 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
Pull Request Review: Better Auth Migration & Schema Validation FixesThis PR addresses schema validation issues from the Clerk to Better Auth migration by implementing a comprehensive cleanup utility for orphaned data. Critical Issues Found1. Security: Missing Access ControlThe 2. Performance: O(n²) ComplexityThe cleanup function loads ALL records into memory using 3. Type Safety: Excessive
|
Detailed RecommendationsFix 1: Add Authentication to Cleanup ActionThe cleanupOrphanedProjectsAction needs authentication. Currently anyone can trigger mass deletion. Recommended fix in convex/importData.ts:512-523:
Fix 2: Optimize Performance with Indexed QueriesCurrent approach loads ALL projects/usage/oauth/imports into memory which will not scale. Recommendation:
Fix 3: Remove Type Safety Issues10+ 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 ControlAuth 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 CoverageNo tests for cleanup logic. Add tests to verify:
What Works Well✅ Cascade deletion properly ordered (attachments -> fragments -> messages -> projects) Overall: Great migration work, but needs security and performance hardening before production. |
Review SummaryStatus: Critical Issues: 2
Medium Issues: 3
Positive: Excellent documentation, well-structured logic, solves the orphaned data problem Next Steps:
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. |
Summary by CodeRabbit
Bug Fixes
New Features
Chores