Skip to content

Conversation

ajanraj
Copy link
Owner

@ajanraj ajanraj commented Sep 21, 2025

Summary

Complete migration from @convex-dev/auth to Better Auth for improved flexibility, cleaner APIs, and better TypeScript support.

Key Changes

Authentication Infrastructure:

  • Added Better Auth 1.3.8 and @convex-dev/better-auth 0.8.6
  • Configured convex.config.ts with Better Auth app
  • Set up auth.ts with Google OAuth and anonymous authentication
  • Created Next.js API route handlers at /api/auth/[...all]

Frontend Updates:

  • Created auth-client.ts with Convex and anonymous plugins
  • Updated ConvexBetterAuthProvider integration
  • Migrated auth components to use authClient.signIn methods
  • Updated user provider to use Better Auth hooks

Backend Integration:

  • Updated all Convex queries/mutations to use authComponent.safeGetAuthUser(ctx)
  • Added user creation triggers to preserve existing data
  • Updated auth helpers and database functions
  • All API routes updated for Better Auth integration

Data Preservation:

  • User linking triggers maintain existing user IDs
  • All chats, messages, files, and API keys preserved
  • Subscription data remains intact via userId mapping

Cleanup:

  • Removed deprecated feedback functionality
  • Deleted old middleware.ts
  • Updated schema to work with Better Auth
  • Regenerated API types

Migration Commits

  1. Core Infrastructure: Auth config, Better Auth client, API routes
  2. Frontend Providers: ConvexBetterAuthProvider and user provider updates
  3. Auth Components: Sign-in methods and auth dialogs
  4. API Routes: Server-side auth integration
  5. Backend Functions: Convex queries/mutations migration
  6. Schema & Cleanup: Final cleanup and type generation

Test Plan

  • Anonymous user creation works
  • Google OAuth login flow
  • Existing user data accessible after login
  • API endpoints require proper authentication
  • Chat creation and messaging
  • File uploads and management
  • User preferences and settings
  • Session persistence and logout

Breaking Changes

None - this is a drop-in replacement maintaining existing functionality.

Technical Notes

  • Uses safeGetAuthUser(ctx) for safe authentication checks
  • User triggers handle data linking automatically
  • Better Auth creates parallel tables, doesn't replace existing ones
  • All existing user IDs and relationships preserved

Summary by cubic

Migrate authentication from Convex Auth to Better Auth across the app, keeping all user IDs and data intact. This delivers cleaner APIs, anonymous and Google sign-in via Better Auth, and stronger TypeScript support with no breaking changes.

  • Migration

    • Add Better Auth route handlers at /api/auth/[...all] and register Convex HTTP routes.
    • Introduce authClient (React) and getToken helper; replace signIn("google"/"anonymous") with Better Auth methods.
    • Replace getAuthUserId/convexAuthNextjsToken with authComponent.safeGetAuthUser(ctx) in all Convex queries, mutations, and API routes.
    • Preserve existing users via linking triggers; chats, messages, files, API keys, and subscriptions remain mapped to the same userId.
    • Update providers to ConvexBetterAuthProvider; remove old middleware and deprecated feedback; adjust schema and regenerate types.
  • Dependencies

    • Add better-auth 1.3.8 and @convex-dev/better-auth 0.8.6; configure convex.config.ts and auth.config.ts.
    • Remove @convex-dev/auth and @auth/core; bump ai and @ai-sdk/react; update README to Better Auth docs.

Summary by CodeRabbit

  • New Features

    • Google and anonymous sign-in now use Better Auth; unified /api/auth routes and improved sign-in/error flows; account deletion delegated to provider.
  • Refactor

    • App-wide migration to Better Auth for auth flows, providers, client/server helpers, and token handling.
  • Removals/Breaking Changes

    • Feedback submission removed; rate limits simplified to a single authenticated daily bucket.
  • Documentation

    • README, Privacy, and Security pages updated to Better Auth terminology and links.
  • Chores

    • Dependencies switched to Better Auth packages; migrations added for user data cleanup.

- Add Better Auth and Convex Better Auth packages
- Configure convex.config.ts with Better Auth app
- Set up auth.config.ts with Google OAuth and anonymous auth
- Create auth.ts with client and trigger configuration
- Add auth client and server utilities in lib/
- Set up Next.js auth API route handlers

This establishes the foundation for migrating from Convex Auth to Better Auth,
providing improved flexibility, cleaner APIs, and better TypeScript support.
- Replace ConvexReactClient with ConvexBetterAuthProvider
- Integrate authClient from lib/auth-client.ts
- Update user provider to use Better Auth hooks and methods
- Configure layout to use new auth provider structure

This establishes the React context and providers needed for Better Auth
integration while maintaining existing user experience patterns.
- Replace signIn("anonymous") with authClient.signIn.anonymous()
- Replace signIn("google") with authClient.signIn.social({ provider: "google" })
- Update auth dialogs and popovers to use Better Auth methods
- Migrate auth page to use new Better Auth client patterns

This updates all user-facing authentication components to use the new
Better Auth API while maintaining the same user experience.
- Update chat API route to use Better Auth user context
- Migrate Composio integration routes to new auth patterns
- Update rate limiting to work with Better Auth user identification
- Ensure all API routes properly handle Better Auth authentication

This maintains API functionality while adapting to the new Better Auth
authentication system for server-side route handlers.
- Use authComponent.safeGetAuthUser(ctx) for safe authentication checks
- Update auth helper functions to work with Better Auth user object structure
- Migrate all queries/mutations in chats, messages, files, users, api_keys
- Update subscription and connector functions for new auth system
- Preserve existing data relationships and user ID mappings

This completes the backend migration while maintaining all existing
functionality and data integrity through Better Auth triggers.
- Update Convex schema to work with Better Auth user structure
- Remove deprecated feedback functionality and middleware
- Update hooks and pages to use new auth patterns
- Regenerate Convex API types for Better Auth integration
- Update HTTP handlers and instrumentation for new auth system
- Update README documentation for Better Auth setup

This completes the migration from Convex Auth to Better Auth,
removing deprecated code and ensuring consistency across the application.
Copy link

vercel bot commented Sep 21, 2025

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

1 Skipped Deployment
Project Deployment Preview Comments Updated (UTC)
open-chat Ignored Ignored Sep 22, 2025 1:32am

Copy link

coderabbitai bot commented Sep 21, 2025

Note

Other AI code review bot(s) detected

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

Walkthrough

Project migrates authentication from Convex Auth to Better Auth across frontend, server, and Convex layers; adds Better Auth routes and clients, rewires token retrieval and server auth checks, removes feedback schema/mutation, adjusts schema and migrations, updates providers and docs/copy.

Changes

Cohort / File(s) Summary
Docs & copy updates
README.md, app/privacy/page.tsx, app/security/page.tsx
Replace "Convex Auth" references with "Better Auth" and update related guide links and setup wording.
Next.js Better Auth routes
app/api/auth/[...all]/route.ts
Add unified GET/POST handlers via nextJsHandler() to expose /api/auth/* endpoints.
API routes: token retrieval swap
app/api/chat/route.ts, app/api/composio/connect/route.ts, app/api/composio/disconnect/route.ts, app/api/composio/status/route.ts, app/api/create-chat/route.ts, app/api/rate-limits/route.ts
Replace convexAuthNextjsToken() with getToken() from @/lib/auth-server; add early 401 guard when token missing; preserve existing control flow otherwise.
UI auth invocations
app/auth/page.tsx, app/components/auth/anonymous-sign-in.tsx, app/components/chat-input/popover-content-auth.tsx, app/components/chat/dialog-auth.tsx, app/settings/page.tsx
Replace useAuthActions/signIn/legacy sign-out/delete flows with authClient.signIn.*, authClient.signOut, and authClient.deleteUser(); add local error handling and loading state adjustments.
Providers & layout
app/layout.tsx, app/providers/convex-client-provider.tsx, app/providers/user-provider.tsx
Remove ConvexAuthNextjsServerProvider, introduce ConvexBetterAuthProvider wired with authClient, simplify provider stack, and update query gating/skip logic and sign-in/out wiring.
Client/server auth helpers
lib/auth-client.ts, lib/auth-server.ts, middleware.ts, app/api/auth/[...all]/route.ts
Add frontend authClient, export server getToken() wrapper for Better Auth Next.js token helper; remove legacy middleware export.
Convex auth core & wiring
convex/auth.ts, convex/http.ts, convex/convex.config.ts, convex/auth.config.ts
Introduce authComponent via Better Auth client, createAuth factory and triggers (onCreate/onUpdate/onDelete); register Better Auth middleware/routes; add site URL fallback in auth config.
Convex modules: auth integration & guards
convex/api_keys.ts, convex/chats.ts, convex/connectors.ts, convex/files.ts, convex/import_export.ts, convex/lib/auth_helper.ts, convex/messages.ts, convex/users.ts, convex/subscription.ts, convex/scheduled_tasks.ts, convex/lib/cleanup_helper.ts
Replace getAuthUserId(ctx) with authComponent.safeGetAuthUser(ctx)/ctx.auth identity checks, derive userId from authUser.userId, adjust guards/returns, update some function signatures (cleanup helpers) and rate-limit reset signature.
Schema changes & removal
convex/schema.ts, convex/schema/feedback.ts, convex/feedback.ts
Remove authTables spread and the Feedback table and its createFeedback mutation.
User schema adjustments
convex/schema/user.ts
Reposition/add temporary email field at top and move emailVerificationTime; add migration comments.
Migrations
convex/migrations/cleanUserData.ts, convex/migrations/removeDuplicateUserFields.ts
Add internal migrations to clean or remove Better Auth-managed duplicate user fields across users.
Removed/modified utilities
convex/lib/cleanup_helper.ts, convex/messages.ts
Remove userId parameter from cleanup helpers and adapt call sites; adjust branched chat cleanup queries to use new index.
Tooling & deps / telemetry
package.json, instrumentation-client.ts
Swap auth deps to @convex-dev/better-auth / better-auth and disable PostHog debug flag.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User (Browser)
  participant C as Next.js Client/UI
  participant AR as /api/auth/[...all] (Next API)
  participant BA as Better Auth handler
  participant CV as Convex authComponent
  participant DB as Convex DB

  U->>C: Click "Sign in with Google"
  C->>AR: GET /api/auth/signin?provider=google
  AR->>BA: nextJsHandler()
  BA->>CV: run provider flow (Google)
  CV->>DB: onCreate/onUpdate link or create user
  DB-->>CV: user record
  CV-->>BA: session established
  BA-->>C: redirect / callback with session
  C-->>U: signed-in UI
Loading
sequenceDiagram
  autonumber
  participant U as User (Browser)
  participant API as Next.js API (e.g., /api/chat)
  participant GS as getToken (lib/auth-server)
  participant BA as Better Auth Next.js token helper
  participant CX as Convex server-side (uses authComponent)
  participant DB as Convex DB

  U->>API: POST request (with cookies)
  API->>GS: getToken()
  GS->>BA: getToken(createAuth)
  BA-->>GS: token/session (or null)
  GS-->>API: token
  alt token present
    API->>CX: call Convex with token
    CX->>DB: query/mutate using authComponent.safeGetAuthUser
    DB-->>CX: result
    CX-->>API: response
    API-->>U: 200 OK + data
  else no token
    API-->>U: 401 Unauthorized
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested labels

enhancement

Poem

A rabbit hums beside the stack, ears twitch to say hello—
Better Auth in burrowed code, sessions learn to flow.
Tokens hop from client to server, routes now neat and trim,
We prune old fields and tidy paths, the garden's edged and prim.
A carrot cheer for cleaner hops — release day, soft and slow. 🥕

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "feat: migrate from Convex Auth to Better Auth" is a short, single sentence that accurately and concisely summarizes the PR’s primary change—migrating authentication from Convex Auth to Better Auth across configs, backend triggers, frontend providers, and routes—which matches the provided PR objectives and file-level summaries. It uses conventional commit style, contains no extraneous detail, and is clear enough for a reviewer scanning history to understand the main change.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/better-auth-migration

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

❤️ Share

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

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

14 issues found across 46 files

Prompt for AI agents (all 14 issues)

Understand the root cause of the following 14 issues and fix them.


<file name="convex/subscription.ts">

<violation number="1" location="convex/subscription.ts:20">
Unconditionally resetting only authenticatedDaily on subscription update can leave anonymousDaily stale; consider clearing both daily limiters to ensure consistency.</violation>

<violation number="2" location="convex/subscription.ts:36">
Resetting only &quot;authenticatedDaily&quot; may leave &quot;anonymousDaily&quot; uncleared for anonymous users, causing inconsistent rate limits if consumption still uses anonymousDaily elsewhere.</violation>
</file>

<file name="convex/auth.config.ts">

<violation number="1" location="convex/auth.config.ts:5">
Fallback uses undefined env var NEXT_PUBLIC_CONVEX_SITE_URL; use NEXT_PUBLIC_CONVEX_URL for consistency to avoid misconfiguration.</violation>
</file>

<file name="convex/files.ts">

<violation number="1" location="convex/files.ts:22">
Use safeGetAuthUser in checkUpload to ensure a mapped userId before issuing upload URLs; current identity-only check is inconsistent with onUpload and may lead to failed uploads after URL issuance.</violation>
</file>

<file name="convex/migrations/cleanUserData.ts">

<violation number="1" location="convex/migrations/cleanUserData.ts:7">
Loading the entire users table into memory may not scale; consider processing in pages/chunks to avoid timeouts and high memory usage.</violation>

<violation number="2" location="convex/migrations/cleanUserData.ts:20">
Unbounded concurrency via Promise.all can overwhelm the database; prefer sequential or bounded-concurrency/chunked updates.</violation>
</file>

<file name="convex/users.ts">

<violation number="1" location="convex/users.ts:50">
authUser.name can be null; coalesce to undefined to satisfy v.optional(v.string()) and avoid runtime validation errors.</violation>

<violation number="2" location="convex/users.ts:51">
authUser.email can be null; coalesce to undefined to satisfy v.optional(v.string()) and avoid runtime validation errors.</violation>

<violation number="3" location="convex/users.ts:102">
Throwing on unauthenticated here changes the API contract (previously returned null), risking breakage for clients expecting v.null(). Consider returning null to preserve behavior.</violation>

<violation number="4" location="convex/users.ts:680">
Account deletion does not delete the Better Auth user, so onDelete triggers won’t run and auth records/sessions may persist. Ensure the Better Auth user is deleted as part of this flow.</violation>
</file>

<file name="convex/migrations/removeDuplicateUserFields.ts">

<violation number="1" location="convex/migrations/removeDuplicateUserFields.ts:7">
collect() loads the entire users table into memory; consider processing in chunks to avoid memory/time spikes.</violation>

<violation number="2" location="convex/migrations/removeDuplicateUserFields.ts:27">
Using ctx.db.replace overwrites the whole user doc and can drop fields not listed in cleanedUser (data loss risk). Prefer patch with explicit undefined deletions.</violation>

<violation number="3" location="convex/migrations/removeDuplicateUserFields.ts:30">
Issuing all updates in parallel via Promise.all can overwhelm resources for large user sets; consider batching or sequential processing.</violation>
</file>

<file name="app/components/auth/anonymous-sign-in.tsx">

<violation number="1" location="app/components/auth/anonymous-sign-in.tsx:25">
Rule violated: **Enforce Accessibility Standards (WCAG...)**

Error message isn’t announced to screen readers; mark it as a status message with role=&quot;alert&quot; (WCAG 2.1 AA 4.1.3 Status Messages).</violation>
</file>

React with 👍 or 👎 to teach cubic. Mention @cubic-dev-ai to give feedback, ask questions, or re-run the review.

args: {},
handler: async (ctx) => {
// Get all users
const allUsers = await ctx.db.query("users").collect();
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Sep 21, 2025

Choose a reason for hiding this comment

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

Loading the entire users table into memory may not scale; consider processing in pages/chunks to avoid timeouts and high memory usage.

Prompt for AI agents
Address the following comment on convex/migrations/cleanUserData.ts at line 7:

<comment>Loading the entire users table into memory may not scale; consider processing in pages/chunks to avoid timeouts and high memory usage.</comment>

<file context>
@@ -0,0 +1,24 @@
+  args: {},
+  handler: async (ctx) =&gt; {
+    // Get all users
+    const allUsers = await ctx.db.query(&quot;users&quot;).collect();
+
+    // Update each user to remove duplicate fields that Better Auth now manages
</file context>
Fix with Cubic

return null;
const authUser = await authComponent.safeGetAuthUser(ctx);
if (!authUser?.userId) {
throw new ConvexError(ERROR_CODES.NOT_AUTHENTICATED);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Sep 21, 2025

Choose a reason for hiding this comment

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

Throwing on unauthenticated here changes the API contract (previously returned null), risking breakage for clients expecting v.null(). Consider returning null to preserve behavior.

Prompt for AI agents
Address the following comment on convex/users.ts at line 102:

<comment>Throwing on unauthenticated here changes the API contract (previously returned null), risking breakage for clients expecting v.null(). Consider returning null to preserve behavior.</comment>

<file context>
@@ -78,17 +97,17 @@ export const updateUserProfile = mutation({
-      return null;
+    const authUser = await authComponent.safeGetAuthUser(ctx);
+    if (!authUser?.userId) {
+      throw new ConvexError(ERROR_CODES.NOT_AUTHENTICATED);
     }
-    const user = await ctx.db.get(userId);
</file context>

✅ Addressed in a393324

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

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

⚠️ Outside diff range comments (4)
app/api/create-chat/route.ts (1)

53-71: PostHog server config: use server‑side key and EU host to match client data residency

You’re using NEXT_PUBLIC_POSTHOG_KEY on the server and not specifying the EU host. This can route events to the default US endpoint and mixes public env on the server. Prefer a server‑only key and set host explicitly to EU to match the client.

Apply:

-    if (process.env.NEXT_PUBLIC_POSTHOG_KEY) {
+    if (process.env.POSTHOG_KEY) {
       try {
-        const posthog = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY);
+        const posthog = new PostHog(process.env.POSTHOG_KEY, {
+          host: process.env.POSTHOG_HOST ?? "https://eu.posthog.com",
+        });
         posthog.capture({
           distinctId: user._id,
           event: "chat_created",
           properties: {
             model,
           },
         });
         posthog.shutdown().catch((_error) => {

If you intentionally use the same (public) key for both, still set host to EU for consistency.

convex/convex.config.ts (1)

9-13: Register betterAuth before other middleware.
Move app.use(betterAuth) to the top of convex/convex.config.ts so it populates ctx.auth for rateLimiter, polar, resend, r2, etc.; currently it's last (lines 9–13).

app/api/auth/[...all]/route.ts (1)

1-6: Harden auth route handler: force Node.js runtime & disable caching

Auth endpoints must be dynamic and non-cacheable; Better Auth recommends the Node.js runtime.

File: app/api/auth/[...all]/route.ts

 import { nextJsHandler } from "@convex-dev/better-auth/nextjs";

 // Export GET and POST handlers for Better Auth
 // This handles all auth routes like /api/auth/signin, /api/auth/callback/google, etc.
 export const { GET, POST } = nextJsHandler();
+
+// Ensure no caching or static optimization
+export const dynamic = "force-dynamic";
+export const revalidate = 0;
+export const fetchCache = "force-no-store";
+// Consider setting runtime based on Better Auth guidance:
+// export const runtime = "nodejs";

Also set runtime explicitly: export const runtime = "nodejs";

convex/files.ts (1)

35-46: Validate key ownership on duplicate detection.

If a row exists for a different user, fail early to avoid cross-user confusion.

-      if (existing) {
-        return;
-      }
+      if (existing) {
+        if (existing.userId !== userId) {
+          throw new ConvexError(ERROR_CODES.UNAUTHORIZED);
+        }
+        return;
+      }
🧹 Nitpick comments (32)
convex/schema/user.ts (1)

10-10: Clarify timestamp units (prefer explicit “ms” suffix or comment)

Ambiguous time units cause subtle bugs. Either rename to emailVerifiedAtMs in a follow-up migration or document units now.

-  emailVerificationTime: v.optional(v.number()),
+  // Epoch milliseconds; consider renaming to `emailVerifiedAtMs` in a follow-up migration
+  emailVerificationTime: v.optional(v.number()),
instrumentation-client.ts (1)

10-11: Gate PostHog debug via env to aid local troubleshooting without changing code

Hard‑disabling debug makes local diagnostics harder. Prefer an env‑gated switch.

Apply:

-    debug: false,
+    debug:
+      process.env.NEXT_PUBLIC_POSTHOG_DEBUG === "1" ||
+      process.env.NODE_ENV === "development",
app/api/create-chat/route.ts (1)

28-35: Early return on missing token for consistency and fewer unnecessary queries

In other routes (e.g., rate limits) you short‑circuit when the token is absent. Do the same here to avoid an extra Convex round trip.

Apply:

-    const token = await getToken();
-
-    const user = await fetchQuery(api.users.getCurrentUser, {}, { token });
+    const token = await getToken();
+    if (!token) {
+      return createErrorResponse(new Error("Unauthorized"));
+    }
+    const user = await fetchQuery(api.users.getCurrentUser, {}, { token });
app/hooks/use-model-preferences.ts (1)

53-54: Avoid O(n²) includes in filter by using a Set

Minor perf tidy‑up when lists grow.

Apply:

-    const newDisabled = currentDisabled.filter(
-      (id: string) => !newFavorites.includes(id)
-    );
+    const favoritesSet = new Set(newFavorites);
+    const newDisabled = currentDisabled.filter(
+      (id: string) => !favoritesSet.has(id)
+    );
app/api/rate-limits/route.ts (1)

8-13: Return a 401 JSON response directly for unauthorized

Keeps error semantics explicit without invoking generic error handling.

Apply:

-    // If no valid token, the user is not authenticated
-    if (!token) {
-      return createErrorResponse(new Error("Unauthorized"));
-    }
+    // If no valid token, the user is not authenticated
+    if (!token) {
+      return new Response(JSON.stringify({ error: "Unauthorized" }), {
+        status: 401,
+        headers: { "Content-Type": "application/json" },
+      });
+    }
lib/auth-server.ts (1)

1-7: Make this module server-only and add an explicit return type for stronger TS guarantees

  • Prevent accidental client bundling with Next.js by marking this module server-only.
  • Add an explicit return type to satisfy strict mode and document behavior.
+import "server-only";
 import { getToken as getTokenNextjs } from "@convex-dev/better-auth/nextjs";
 import { createAuth } from "../convex/auth";

 // Helper function to get authentication token for server-side operations
-export const getToken = () => {
-  return getTokenNextjs(createAuth);
-};
+export const getToken = (): Promise<string | null | undefined> => {
+  return getTokenNextjs(createAuth);
+};
app/api/composio/disconnect/route.ts (1)

23-38: Harden JSON parsing and type‑narrowing with zod to avoid unsound ConnectorType assignment

connectorType is declared as ConnectorType but populated from untyped JSON, which can be undefined. Use schema parsing to validate and narrow before use.

-import type { ConnectorType } from "@/lib/types";
+import type { ConnectorType } from "@/lib/types";
+import { z } from "zod";
@@
-    let connectorType: ConnectorType;
-    try {
-      ({ connectorType } = await request.json());
-    } catch (_error) {
-      return NextResponse.json(
-        { error: "Invalid JSON in request body" },
-        { status: 400 }
-      );
-    }
-
-    if (!SUPPORTED_CONNECTORS.includes(connectorType)) {
+    const schema = z.object({ connectorType: z.string() });
+    const parsed = schema.safeParse(await request.json());
+    if (!parsed.success) {
+      return NextResponse.json(
+        { error: "Invalid JSON in request body" },
+        { status: 400 }
+      );
+    }
+    const { connectorType } = parsed.data;
+
+    if (!SUPPORTED_CONNECTORS.includes(connectorType as ConnectorType)) {
       return NextResponse.json(
         { error: "Invalid connector type" },
         { status: 400 }
       );
     }
app/api/composio/connect/route.ts (1)

23-46: Validate and narrow request body with zod; avoid ad‑hoc checks

Move JSON parsing and validation into a schema to avoid stringConnectorType casts and duplicate checks.

+import { z } from "zod";
@@
-    let connectorType: string;
-    try {
-      const body = await request.json();
-      connectorType = body.connectorType;
-    } catch (_error) {
-      return NextResponse.json(
-        { error: "Invalid JSON in request body" },
-        { status: 400 }
-      );
-    }
-
-    if (!connectorType) {
-      return NextResponse.json(
-        { error: "Missing connector type" },
-        { status: 400 }
-      );
-    }
-
-    if (!SUPPORTED_CONNECTORS.includes(connectorType as ConnectorType)) {
+    const schema = z.object({ connectorType: z.string() });
+    const parsed = schema.safeParse(await request.json());
+    if (!parsed.success) {
+      return NextResponse.json(
+        { error: "Invalid JSON in request body" },
+        { status: 400 }
+      );
+    }
+    const { connectorType } = parsed.data;
+
+    if (!SUPPORTED_CONNECTORS.includes(connectorType as ConnectorType)) {
       return NextResponse.json(
         { error: "Invalid connector type" },
         { status: 400 }
       );
     }
app/components/auth/anonymous-sign-in.tsx (1)

12-18: Avoid setting state on unmounted component

Wrap the async call with an “isMounted” guard to prevent React warnings if the user navigates away mid‑request.

   useEffect(() => {
-    if (!attemptedAnon.current) {
-      attemptedAnon.current = true;
-      authClient.signIn.anonymous().catch(() => {
-        setError("Failed to sign in. Please refresh the page.");
-      });
-    }
+    if (attemptedAnon.current) return;
+    attemptedAnon.current = true;
+    let mounted = true;
+    authClient.signIn.anonymous().catch(() => {
+      if (mounted) setError("Failed to sign in. Please refresh the page.");
+    });
+    return () => {
+      mounted = false;
+    };
   }, []);
app/hooks/use-model-settings.ts (1)

30-31: TS strictness improvements: LGTM; minor immutability nit

The added (id: string) annotations are good. To avoid aliasing the original array, clone currentFavorites before modifications.

-    let newFavorites = currentFavorites;
+    let newFavorites = [...currentFavorites];

Also applies to: 40-47, 73-80, 88-89

app/components/chat/dialog-auth.tsx (2)

68-74: Add explicit button type.

Buttons must include type to avoid implicit “submit” behavior if used inside forms later.

-          <Button
+          <Button
+            type="button"
             className="w-full text-base"
             disabled={isLoading}
             onClick={handleSignInWithGoogle}
             size="lg"
             variant="secondary"
           >

75-81: A11y + asset sourcing: make the icon decorative (or use Phosphor).

  • Decorative icon should not duplicate the button’s accessible name.
  • Avoid remote favicon dependency; prefer Phosphor’s Google logo icon.

Option A (quick, keep Image):

-            <Image
-              alt="Google logo"
+            <Image
+              alt=""
+              aria-hidden
               className="mr-2 size-4"
               height={20}
               src="https://www.google.com/favicon.ico"
+              unoptimized
               width={20}
             />

Option B (preferred per guidelines: use Phosphor icon):

+import { GoogleLogo as GoogleLogoIcon } from "@phosphor-icons/react";
...
-            <Image ... />
+            <GoogleLogoIcon aria-hidden className="mr-2 size-4" weight="fill" />

If you keep the remote image, confirm next.config.js permits google.com under images.remotePatterns or use unoptimized as shown.

convex/import_export.ts (1)

47-52: Reuse ensureAuthenticated to avoid duplication and unsafe casts.

You can lean on the shared helper and drop the as Id<"users">.

-import { authComponent } from "./auth";
+import { ensureAuthenticated } from "./lib/auth_helper";
...
-    const authUser = await authComponent.safeGetAuthUser(ctx);
-    if (!authUser?.userId) {
-      throw new ConvexError(ERROR_CODES.NOT_AUTHENTICATED);
-    }
-    const userId = authUser.userId as Id<"users">;
+    const userId = await ensureAuthenticated(ctx);

Confirm the user-creation trigger (linking Better Auth users to existing IDs) fires before this mutation runs so userId always points to an existing users doc.

app/layout.tsx (1)

165-174: Provider order looks sound; consider analytics placement.

Placing Analytics/SpeedInsights inside AuthGuard delays them until after auth completes. If you want metrics on unauth flows (loading/anonymous), move them outside.

-          <ConvexClientProvider>
-            <AuthGuard>
-              <SidebarProvider>
-                <LayoutApp>{children}</LayoutApp>
-              </SidebarProvider>
-              <Analytics />
-              <SpeedInsights />
-            </AuthGuard>
-          </ConvexClientProvider>
+          <ConvexClientProvider>
+            <AuthGuard>
+              <SidebarProvider>
+                <LayoutApp>{children}</LayoutApp>
+              </SidebarProvider>
+            </AuthGuard>
+            <Analytics />
+            <SpeedInsights />
+          </ConvexClientProvider>

If the current placement is intentional (to avoid logging pre-auth noise), ignore.

convex/lib/auth_helper.ts (1)

5-5: Auth helper migration is solid.

Use of authComponent.safeGetAuthUser(ctx) with centralized throwing in ensureAuthenticated and a nullable variant in getCurrentUserOrNull is clean.

If you find yourself needing just the userId optionally in more places, consider adding:

export async function getAuthUserIdOrNull(ctx: QueryCtx | MutationCtx): Promise<Id<"users"> | null> {
  const authUser = await authComponent.safeGetAuthUser(ctx);
  return (authUser?.userId as Id<"users">) ?? null;
}

Also applies to: 17-22, 110-115

app/auth/page.tsx (3)

16-33: Reset loading state in finally.

Ensure the button re-enables if the call returns without redirect (popup blockers, etc.).

   async function handleSignInWithGoogle() {
     try {
       setIsLoading(true);
       setError(null);

       await authClient.signIn.social({
         provider: "google",
       });
-    } catch (err: unknown) {
-      setIsLoading(false);
+    } catch (err: unknown) {
       // console.error('Error signing in with Google:', err);
       if (err instanceof Error) {
         setError(err.message);
       } else {
         setError("An unexpected error occurred. Please try again.");
       }
+    } finally {
+      setIsLoading(false);
     }
   }

59-65: Add explicit button type.

-              <Button
+              <Button
+                type="button"
                 className="h-14 w-full text-lg"
                 disabled={isLoading}
                 onClick={handleSignInWithGoogle}
                 size="lg"
                 variant="secondary"
               >

66-73: A11y + icon source: make decorative or use Phosphor.

Same rationale as dialog-auth.

Option A (quick):

-                <Image
-                  alt="Google logo"
+                <Image
+                  alt=""
+                  aria-hidden
                   className="mr-3 size-6"
                   height={24}
                   src="https://www.google.com/favicon.ico"
                   unoptimized
                   width={24}
                 />

Option B (preferred):

+import { GoogleLogo as GoogleLogoIcon } from "@phosphor-icons/react";
...
-                <Image ... />
+                <GoogleLogoIcon aria-hidden className="mr-3 size-6" weight="fill" />

Confirm design prefers Phosphor icons per guidelines; otherwise keep Option A.

convex/messages.ts (1)

392-397: Deduplicate userId retrieval; avoid repeated casts.

Create a tiny helper that returns a properly typed Id<"users"> | null and use it here to remove as casts and keep semantics consistent where you want “return empty” rather than throw.

Apply this diff locally and replace in both call sites:

+// convex/lib/auth_user.ts
+import type { Id } from "../_generated/dataModel";
+import { authComponent } from "../auth";
+export async function getOptionalUserId(
+  ctx: Parameters<typeof authComponent.safeGetAuthUser>[0],
+): Promise<Id<"users"> | null> {
+  const u = await authComponent.safeGetAuthUser(ctx);
+  return (u?.userId as Id<"users">) ?? null;
+}

And in this file:

-const authUser = await authComponent.safeGetAuthUser(ctx);
-if (!authUser?.userId) {
+const userId = await getOptionalUserId(ctx);
+if (!userId) {
   return { chatDeleted: false };
 }
-const userId = authUser.userId as Id<"users">;
-const authUser = await authComponent.safeGetAuthUser(ctx);
-if (!authUser?.userId || search.trim() === "") {
+const userId = await getOptionalUserId(ctx);
+if (!userId || search.trim() === "") {
   return [];
 }
-const userId = authUser.userId as Id<"users">;

Also applies to: 530-535

convex/subscription.ts (1)

32-46: Also reset anonymousDaily in helper.

Include anonymousDaily alongside authenticatedDaily.

Apply:

 async function resetUserRateLimits(ctx: MutationCtx, userId: Id<"users">) {
   // Reset daily limits (subscription users are always authenticated)
   // Run all rate limiter operations concurrently
   const operations = [
+    // Clear possible residual anonymous usage
+    rateLimiter.reset(ctx, "anonymousDaily", { key: userId }),
+    rateLimiter.limit(ctx, "anonymousDaily", { key: userId, count: 0 }),
     rateLimiter.reset(ctx, "authenticatedDaily", { key: userId }),
     rateLimiter.limit(ctx, "authenticatedDaily", { key: userId, count: 0 }),
     rateLimiter.reset(ctx, "standardMonthly", { key: userId }),
     rateLimiter.limit(ctx, "standardMonthly", { key: userId, count: 0 }),
     rateLimiter.reset(ctx, "premiumMonthly", { key: userId }),
     rateLimiter.limit(ctx, "premiumMonthly", { key: userId, count: 0 }),
   ];
convex/api_keys.ts (4)

7-12: Add key versioning to support API_KEY_SECRET rotation without breaking decryption.

Store a keyVersion with each record; derive keys per version to allow future rotations/migrations.

Apply minimal scaffolding (schema will need a keyVersion field):

+const KEY_VERSION = Number(process.env.API_KEY_SECRET_VERSION ?? "1");
-      await ctx.db.patch(existing._id, {
+      await ctx.db.patch(existing._id, {
         encryptedKey: encrypted,
         mode: finalMode,
         updatedAt: now,
+        keyVersion: KEY_VERSION,
       });
-      await ctx.db.insert("user_api_keys", {
+      await ctx.db.insert("user_api_keys", {
         userId,
         provider,
         encryptedKey: encrypted,
+        keyVersion: KEY_VERSION,
         mode: finalMode,
         messageCount: 0,
         createdAt: now,
         updatedAt: now,
       });

Also applies to: 68-100, 102-123, 125-162


370-372: Increment should also bump updatedAt.

Keeps timestamps meaningful for usage analytics.

-      await ctx.db.patch(existing._id, { messageCount: currentCount + 1 });
+      await ctx.db.patch(existing._id, {
+        messageCount: currentCount + 1,
+        updatedAt: Date.now(),
+      });

319-348: Declare explicit returns validator for getDecryptedKey.

Improves API contract and runtime validation.

-export const getDecryptedKey = query({
-  args: {
+export const getDecryptedKey = query({
+  args: {
     provider: v.union(
       v.literal("openrouter"),
       v.literal("openai"),
       v.literal("anthropic"),
       v.literal("gemini"),
       v.literal("mistral"),
       v.literal("meta"),
       v.literal("Qwen")
     ),
   },
+  returns: v.union(v.string(), v.null()),

88-95: PBKDF2 iteration count: consider increasing.

100_000 is acceptable, but consider ≥310k to align with current guidance if latency budget allows.

app/providers/user-provider.tsx (2)

6-7: Good adoption of authClient; add cache invalidation on sign-out.

Ensure UI clears stale user-dependent queries immediately after sign-out.

-import { useQuery as useTanStackQuery } from "@tanstack/react-query";
+import { useQuery as useTanStackQuery, useQueryClient } from "@tanstack/react-query";
...
 export function UserProvider({
@@
   // Auth actions from Better Auth
@@
   const signInGoogle = useCallback(async () => {
     await authClient.signIn.social({ provider: "google" });
   }, []);
@@
-  const signOutUser = useCallback(async () => {
-    await authClient.signOut();
-  }, []);
+  const queryClient = useQueryClient();
+  const signOutUser = useCallback(async () => {
+    await authClient.signOut();
+    // Immediately invalidate user-related queries
+    queryClient.invalidateQueries();
+  }, [queryClient]);

Also applies to: 134-137, 184-187, 193-194, 228-232


86-107: DRY the “authenticated and not anonymous” guard.

Minor readability: compute once and reuse.

+  const isAuthedNonAnon = Boolean(user && !user?.isAnonymous);
@@
-    ...convexQuery(
-      api.users.userHasPremium,
-      user && !user?.isAnonymous ? {} : "skip"
-    ),
+    ...convexQuery(api.users.userHasPremium, isAuthedNonAnon ? {} : "skip"),
@@
-    ...convexQuery(
-      api.polar.getConfiguredProducts,
-      user && !user?.isAnonymous ? {} : "skip"
-    ),
+    ...convexQuery(api.polar.getConfiguredProducts, isAuthedNonAnon ? {} : "skip"),
@@
-    ...convexQuery(
-      api.api_keys.getApiKeys,
-      user && !user?.isAnonymous ? {} : "skip"
-    ),
+    ...convexQuery(api.api_keys.getApiKeys, isAuthedNonAnon ? {} : "skip"),

Also applies to: 112-116, 120-128

convex/files.ts (2)

437-441: Consistent auth helper for getStorageUrl.

Prefer safeGetAuthUser for consistency and to ensure user mapping exists.

-    const authUser = await ctx.auth.getUserIdentity();
-    if (authUser === null) {
+    const authUser = await authComponent.safeGetAuthUser(ctx);
+    if (!authUser?.userId) {
       throw new ConvexError(ERROR_CODES.NOT_AUTHENTICATED);
     }

473-478: Remove unnecessary Promise.all; mapping is synchronous.

Avoid creating/waiting on pointless promises.

-    return Promise.all(
-      attachments.map(async (attachment) => ({
-        ...attachment,
-        url: attachment.url,
-      }))
-    );
+    return attachments.map((attachment) => ({
+      ...attachment,
+      url: attachment.url,
+    }));
convex/auth.ts (2)

105-141: Add explicit types for createAuth params and return

Strengthens API and satisfies strict TS guidelines.

Apply this diff:

-export const createAuth = (
-  ctx: GenericCtx<DataModel>,
-  { optionsOnly } = { optionsOnly: false }
-) => {
+export const createAuth = (
+  ctx: GenericCtx<DataModel>,
+  { optionsOnly }: { optionsOnly?: boolean } = { optionsOnly: false }
+): ReturnType<typeof betterAuth> => {

55-90: Consider removing zero-count rate limit “initialization”

Calling limit(..., { count: 0 }) creates extra writes and may be a no‑op depending on implementation. The components typically initialize lazily on first use.

convex/users.ts (2)

457-517: Rate limit status: prefer consistent key usage type

You use authUser.userId (typed) in some calls and userId (Id<"users">) in others. Keep a single const userId and use it for all RL calls to avoid drift.

Apply this diff snippet pattern:

-    ] = await Promise.all([
+    const userId = authUser.userId as Id<"users">;
+    ] = await Promise.all([
-        : rateLimiter.check(ctx, dailyLimitName, { key: authUser.userId }),
+        : rateLimiter.check(ctx, dailyLimitName, { key: userId }),
-        : rateLimiter.getValue(ctx, dailyLimitName, { key: authUser.userId }),
-      rateLimiter.check(ctx, "standardMonthly", { key: authUser.userId }),
-      rateLimiter.getValue(ctx, "standardMonthly", { key: authUser.userId }),
+        : rateLimiter.getValue(ctx, dailyLimitName, { key: userId }),
+      rateLimiter.check(ctx, "standardMonthly", { key: userId }),
+      rateLimiter.getValue(ctx, "standardMonthly", { key: userId }),
-        ? rateLimiter.check(ctx, "premiumMonthly", { key: authUser.userId })
+        ? rateLimiter.check(ctx, "premiumMonthly", { key: userId })
-        ? rateLimiter.getValue(ctx, "premiumMonthly", { key: authUser.userId })
+        ? rateLimiter.getValue(ctx, "premiumMonthly", { key: userId })

832-863: Backfill caution for internal mass update

This internal mutation updates all users. Gate with an allowlist check or admin assertion to avoid accidental invocation in production.

Add a simple guard, e.g., require an admin flag or internal secret before proceeding.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 434e4ae and a00dfb5.

⛔ Files ignored due to path filters (2)
  • bun.lock is excluded by !**/*.lock
  • convex/_generated/api.d.ts is excluded by !**/_generated/**
📒 Files selected for processing (44)
  • README.md (4 hunks)
  • app/api/auth/[...all]/route.ts (1 hunks)
  • app/api/chat/route.ts (2 hunks)
  • app/api/composio/connect/route.ts (1 hunks)
  • app/api/composio/disconnect/route.ts (1 hunks)
  • app/api/composio/status/route.ts (1 hunks)
  • app/api/create-chat/route.ts (2 hunks)
  • app/api/rate-limits/route.ts (1 hunks)
  • app/auth/page.tsx (1 hunks)
  • app/c/[chatId]/page.tsx (2 hunks)
  • app/components/auth/anonymous-sign-in.tsx (1 hunks)
  • app/components/chat-input/popover-content-auth.tsx (1 hunks)
  • app/components/chat/dialog-auth.tsx (2 hunks)
  • app/hooks/use-model-preferences.ts (2 hunks)
  • app/hooks/use-model-settings.ts (4 hunks)
  • app/layout.tsx (1 hunks)
  • app/privacy/page.tsx (1 hunks)
  • app/providers/convex-client-provider.tsx (3 hunks)
  • app/providers/user-provider.tsx (6 hunks)
  • app/security/page.tsx (1 hunks)
  • convex/api_keys.ts (7 hunks)
  • convex/auth.config.ts (1 hunks)
  • convex/auth.ts (1 hunks)
  • convex/chats.ts (4 hunks)
  • convex/connectors.ts (3 hunks)
  • convex/convex.config.ts (2 hunks)
  • convex/feedback.ts (0 hunks)
  • convex/files.ts (12 hunks)
  • convex/http.ts (1 hunks)
  • convex/import_export.ts (2 hunks)
  • convex/lib/auth_helper.ts (3 hunks)
  • convex/messages.ts (3 hunks)
  • convex/migrations/cleanUserData.ts (1 hunks)
  • convex/migrations/removeDuplicateUserFields.ts (1 hunks)
  • convex/schema.ts (0 hunks)
  • convex/schema/feedback.ts (0 hunks)
  • convex/schema/user.ts (1 hunks)
  • convex/subscription.ts (2 hunks)
  • convex/users.ts (17 hunks)
  • instrumentation-client.ts (1 hunks)
  • lib/auth-client.ts (1 hunks)
  • lib/auth-server.ts (1 hunks)
  • middleware.ts (0 hunks)
  • package.json (2 hunks)
💤 Files with no reviewable changes (4)
  • convex/schema/feedback.ts
  • middleware.ts
  • convex/feedback.ts
  • convex/schema.ts
🧰 Additional context used
📓 Path-based instructions (8)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use TypeScript with strict mode; avoid any/unknown; prefer explicit types/interfaces; do not use @ts-ignore or disable type checks
Don’t use the TypeScript any type; avoid implicit any; don’t use @ts-ignore; prefer export type/import type; avoid non-null (!) misuse
Use arrow functions over function expressions; use const for single-assignment variables; avoid var
Avoid unnecessary constructs: fragments, boolean casts, string concatenation where template literals suffice, useless this aliasing, unnecessary constructors/continue/catch
Prefer for...of and while loops appropriately; avoid Array.forEach when for...of improves clarity/perf
Ensure no unused imports, variables, params, labels, private members; prevent import cycles
Use Number.isNaN / Number.isFinite; always pass radix to parseInt; use Date.now; prefer object spread over Object.assign; prefer template literals over string concatenation
Disallow console/debugger; use ===/!==; avoid duplicate/sparse arrays, duplicate keys/members/cases, reassignment of imports/natives
React: call hooks at top level; specify hook deps correctly; provide keys; avoid array index keys; don’t define components inside components; don’t use event handlers on non-interactive elements
Don’t hardcode secrets; ensure Promise-like results are handled; avoid await in loops; ensure switch statements are exhaustive and default comes last
Use node: protocol for Node built-ins and prefer node:assert/strict
Use with { type: "json" } for JSON module imports
Animation: honor prefers-reduced-motion; animate transform/opacity; avoid layout-affecting properties
Style: don’t use global eval/with; avoid nested/negative ifs; prefer else-if/early return; use ** over Math.pow; prefer assignment shorthand
Testing Best Practices: avoid focused/disabled tests; ensure expect is inside it(); don’t use export/module.exports in test files
Next.js rule: don’t import next/head in pages/_document.*
Security: don’t assign to document.cookie directl...

Files:

  • lib/auth-server.ts
  • instrumentation-client.ts
  • app/components/auth/anonymous-sign-in.tsx
  • app/components/chat/dialog-auth.tsx
  • convex/import_export.ts
  • app/hooks/use-model-preferences.ts
  • convex/connectors.ts
  • app/api/create-chat/route.ts
  • convex/lib/auth_helper.ts
  • app/components/chat-input/popover-content-auth.tsx
  • convex/migrations/removeDuplicateUserFields.ts
  • app/api/composio/disconnect/route.ts
  • app/api/auth/[...all]/route.ts
  • convex/migrations/cleanUserData.ts
  • app/privacy/page.tsx
  • app/api/composio/status/route.ts
  • app/c/[chatId]/page.tsx
  • app/api/rate-limits/route.ts
  • lib/auth-client.ts
  • convex/api_keys.ts
  • app/api/composio/connect/route.ts
  • convex/files.ts
  • convex/messages.ts
  • convex/auth.ts
  • convex/users.ts
  • convex/auth.config.ts
  • app/auth/page.tsx
  • app/security/page.tsx
  • convex/convex.config.ts
  • app/providers/convex-client-provider.tsx
  • convex/subscription.ts
  • convex/chats.ts
  • app/api/chat/route.ts
  • app/layout.tsx
  • app/hooks/use-model-settings.ts
  • convex/schema/user.ts
  • app/providers/user-provider.tsx
  • convex/http.ts
{app,components}/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

Use functional React components; call hooks only at top level; do not use default exports for components or functions

Files:

  • app/components/auth/anonymous-sign-in.tsx
  • app/components/chat/dialog-auth.tsx
  • app/components/chat-input/popover-content-auth.tsx
  • app/privacy/page.tsx
  • app/c/[chatId]/page.tsx
  • app/auth/page.tsx
  • app/security/page.tsx
  • app/providers/convex-client-provider.tsx
  • app/layout.tsx
  • app/providers/user-provider.tsx
**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.tsx: Use Tailwind CSS utility classes for styling and avoid inline styles
Ensure accessibility (semantic HTML, alt text, proper ARIA) per agent_rules/ultracite.md
Use Phosphor React icon components with the Icon suffix (e.g., CaretIcon, not Caret)
Next.js rule: don’t use or elements; use framework-provided components/APIs
A11y: Include meaningful alt text for images; do not include the words “image”, “picture”, or “photo” in alt
A11y: Use semantic elements and valid ARIA roles/states; avoid adding unsupported ARIA to elements
A11y: Buttons must include explicit type attribute
A11y: Provide visible focus, keyboard support, and correct key/mouse event pairing (onClick with keyboard, hover with focus)
A11y: SVGs and iframes must include title; html element must include lang
React/JSX: don’t use dangerous props, don’t mix children with dangerouslySetInnerHTML, avoid extra closing tags, use <> fragments, don’t insert comments as text nodes
Content & a11y: ensure <title> reflects context; icon-only buttons need aria-label; maintain accessible names; add skip links and scroll-margin-top on headings
Performance: virtualize large lists; preload above-the-fold images and lazy-load others; prevent CLS via explicit image dimensions

Files:

  • app/components/auth/anonymous-sign-in.tsx
  • app/components/chat/dialog-auth.tsx
  • app/components/chat-input/popover-content-auth.tsx
  • app/privacy/page.tsx
  • app/c/[chatId]/page.tsx
  • app/auth/page.tsx
  • app/security/page.tsx
  • app/providers/convex-client-provider.tsx
  • app/layout.tsx
  • app/providers/user-provider.tsx
convex/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Follow Convex-specific guidelines in agent_rules/convex_rules.md for queries, mutations, actions, and server logic

Files:

  • convex/import_export.ts
  • convex/connectors.ts
  • convex/lib/auth_helper.ts
  • convex/migrations/removeDuplicateUserFields.ts
  • convex/migrations/cleanUserData.ts
  • convex/api_keys.ts
  • convex/files.ts
  • convex/messages.ts
  • convex/auth.ts
  • convex/users.ts
  • convex/auth.config.ts
  • convex/convex.config.ts
  • convex/subscription.ts
  • convex/chats.ts
  • convex/schema/user.ts
  • convex/http.ts
convex/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Organize Convex API functions: queries/mutations/actions/internal under convex/

Files:

  • convex/import_export.ts
  • convex/connectors.ts
  • convex/lib/auth_helper.ts
  • convex/migrations/removeDuplicateUserFields.ts
  • convex/migrations/cleanUserData.ts
  • convex/api_keys.ts
  • convex/files.ts
  • convex/messages.ts
  • convex/auth.ts
  • convex/users.ts
  • convex/auth.config.ts
  • convex/convex.config.ts
  • convex/subscription.ts
  • convex/chats.ts
  • convex/schema/user.ts
  • convex/http.ts
app/providers/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

Place React Context providers for global state under app/providers/

Files:

  • app/providers/convex-client-provider.tsx
  • app/providers/user-provider.tsx
app/layout.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

Maintain the primary Next.js App Router layout in app/layout.tsx

Files:

  • app/layout.tsx
convex/schema/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Define Convex database schema in convex/schema/ with modular tables

Files:

  • convex/schema/user.ts
🧠 Learnings (14)
📚 Learning: 2025-09-20T14:21:06.336Z
Learnt from: CR
PR: ajanraj/OpenChat#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-20T14:21:06.336Z
Learning: Applies to **/*.tsx : Ensure accessibility (semantic HTML, alt text, proper ARIA) per agent_rules/ultracite.md

Applied to files:

  • app/components/auth/anonymous-sign-in.tsx
📚 Learning: 2025-09-20T14:21:06.336Z
Learnt from: CR
PR: ajanraj/OpenChat#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-20T14:21:06.336Z
Learning: Applies to **/*.tsx : A11y: Use semantic elements and valid ARIA roles/states; avoid adding unsupported ARIA to elements

Applied to files:

  • app/components/auth/anonymous-sign-in.tsx
📚 Learning: 2025-09-20T14:21:06.336Z
Learnt from: CR
PR: ajanraj/OpenChat#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-20T14:21:06.336Z
Learning: Applies to **/*.tsx : Content & a11y: ensure <title> reflects context; icon-only buttons need aria-label; maintain accessible names; add skip links and scroll-margin-top on headings

Applied to files:

  • app/components/auth/anonymous-sign-in.tsx
📚 Learning: 2025-09-20T14:21:06.336Z
Learnt from: CR
PR: ajanraj/OpenChat#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-20T14:21:06.336Z
Learning: Applies to **/*.tsx : A11y: SVGs and iframes must include title; html element must include lang

Applied to files:

  • app/components/auth/anonymous-sign-in.tsx
📚 Learning: 2025-09-20T14:21:06.336Z
Learnt from: CR
PR: ajanraj/OpenChat#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-20T14:21:06.336Z
Learning: Applies to **/*.tsx : A11y: Buttons must include explicit type attribute

Applied to files:

  • app/components/auth/anonymous-sign-in.tsx
📚 Learning: 2025-09-20T14:21:06.336Z
Learnt from: CR
PR: ajanraj/OpenChat#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-20T14:21:06.336Z
Learning: Applies to **/*.tsx : A11y: Provide visible focus, keyboard support, and correct key/mouse event pairing (onClick with keyboard, hover with focus)

Applied to files:

  • app/components/auth/anonymous-sign-in.tsx
📚 Learning: 2025-09-20T14:21:06.336Z
Learnt from: CR
PR: ajanraj/OpenChat#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-20T14:21:06.336Z
Learning: Applies to convex/**/*.{ts,tsx} : Follow Convex-specific guidelines in agent_rules/convex_rules.md for queries, mutations, actions, and server logic

Applied to files:

  • convex/connectors.ts
  • convex/migrations/cleanUserData.ts
  • convex/chats.ts
  • app/providers/user-provider.tsx
📚 Learning: 2025-09-20T14:21:06.336Z
Learnt from: CR
PR: ajanraj/OpenChat#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-20T14:21:06.336Z
Learning: Applies to convex/**/*.ts : Organize Convex API functions: queries/mutations/actions/internal under convex/

Applied to files:

  • convex/migrations/cleanUserData.ts
  • convex/convex.config.ts
  • app/layout.tsx
  • app/providers/user-provider.tsx
📚 Learning: 2025-08-31T22:44:58.866Z
Learnt from: ajanraj
PR: ajanraj/OpenChat#15
File: convex/messages.ts:79-82
Timestamp: 2025-08-31T22:44:58.866Z
Learning: In OpenChat's R2 storage migration (convex/files.ts), the upload follows a three-phase pattern: onUpload creates pending row with userId/key, onSyncMetadata patches R2 metadata (fileType/fileSize), and internalSave patches the final chatId association. The cleanup logic in convex/messages.ts correctly requires attByKey.chatId === chatId since attachments in messages will always have chatId populated after the internalSave phase.

Applied to files:

  • convex/files.ts
📚 Learning: 2025-08-31T22:44:58.866Z
Learnt from: ajanraj
PR: ajanraj/OpenChat#15
File: convex/messages.ts:79-82
Timestamp: 2025-08-31T22:44:58.866Z
Learning: In the OpenChat R2 storage migration, attachments follow a two-phase creation pattern in convex/files.ts: first inserted as pending rows with only userId and key (line 42-45), then later patched with chatId and full metadata via internalSave function during saveFileAttachment. The cleanup logic in convex/messages.ts correctly uses the strict check attByKey.chatId === chatId because attachments referenced in messages will always have chatId populated after the patching phase.

Applied to files:

  • convex/files.ts
📚 Learning: 2025-08-31T22:44:58.866Z
Learnt from: ajanraj
PR: ajanraj/OpenChat#15
File: convex/messages.ts:79-82
Timestamp: 2025-08-31T22:44:58.866Z
Learning: In the R2 storage migration codebase, the saveFileAttachment function in convex/files.ts always sets the chatId field when creating chat_attachment records, so attachments referenced in messages will always have chatId populated. The cleanup logic in convex/messages.ts correctly uses the strict check attByKey.chatId === chatId to prevent accidental deletion of attachments from other chats.

Applied to files:

  • convex/files.ts
📚 Learning: 2025-08-31T22:44:58.866Z
Learnt from: ajanraj
PR: ajanraj/OpenChat#15
File: convex/messages.ts:79-82
Timestamp: 2025-08-31T22:44:58.866Z
Learning: In the OpenChat R2 storage migration, attachments follow a two-phase creation pattern: first inserted as pending rows with only userId and key, then later patched with chatId and full metadata via internalSave. The cleanup logic in convex/messages.ts correctly uses the strict check attByKey.chatId === chatId because attachments referenced in messages will always have chatId populated after the patching phase.

Applied to files:

  • convex/files.ts
📚 Learning: 2025-09-20T14:21:06.336Z
Learnt from: CR
PR: ajanraj/OpenChat#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-20T14:21:06.336Z
Learning: Applies to app/layout.tsx : Maintain the primary Next.js App Router layout in app/layout.tsx

Applied to files:

  • app/layout.tsx
📚 Learning: 2025-09-20T14:21:06.336Z
Learnt from: CR
PR: ajanraj/OpenChat#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-20T14:21:06.336Z
Learning: Applies to app/providers/**/*.tsx : Place React Context providers for global state under app/providers/

Applied to files:

  • app/layout.tsx
🧬 Code graph analysis (27)
lib/auth-server.ts (1)
convex/auth.ts (1)
  • createAuth (105-141)
app/components/auth/anonymous-sign-in.tsx (1)
lib/auth-client.ts (1)
  • authClient (6-11)
app/components/chat/dialog-auth.tsx (1)
lib/auth-client.ts (1)
  • authClient (6-11)
convex/import_export.ts (2)
lib/error-codes.ts (1)
  • ERROR_CODES (65-73)
convex/_generated/dataModel.d.ts (1)
  • Id (48-49)
convex/connectors.ts (2)
convex/auth.ts (2)
  • authComponent (21-103)
  • authComponent (144-144)
convex/_generated/dataModel.d.ts (1)
  • Id (48-49)
app/api/create-chat/route.ts (1)
lib/auth-server.ts (1)
  • getToken (5-7)
convex/lib/auth_helper.ts (1)
convex/_generated/dataModel.d.ts (1)
  • Id (48-49)
app/components/chat-input/popover-content-auth.tsx (1)
lib/auth-client.ts (1)
  • authClient (6-11)
app/api/composio/disconnect/route.ts (2)
app/api/composio/connect/route.ts (1)
  • POST (10-70)
lib/auth-server.ts (1)
  • getToken (5-7)
app/api/composio/status/route.ts (2)
app/api/rate-limits/route.ts (1)
  • GET (6-29)
lib/auth-server.ts (1)
  • getToken (5-7)
app/c/[chatId]/page.tsx (1)
lib/auth-server.ts (1)
  • getToken (5-7)
app/api/rate-limits/route.ts (2)
app/api/composio/status/route.ts (1)
  • GET (8-46)
lib/auth-server.ts (1)
  • getToken (5-7)
convex/api_keys.ts (1)
convex/_generated/dataModel.d.ts (1)
  • Id (48-49)
app/api/composio/connect/route.ts (3)
app/api/composio/disconnect/route.ts (1)
  • POST (10-79)
app/api/create-chat/route.ts (1)
  • POST (8-83)
lib/auth-server.ts (1)
  • getToken (5-7)
convex/files.ts (2)
lib/error-codes.ts (1)
  • ERROR_CODES (65-73)
convex/_generated/dataModel.d.ts (1)
  • Id (48-49)
convex/messages.ts (1)
convex/_generated/dataModel.d.ts (1)
  • Id (48-49)
convex/auth.ts (3)
convex/_generated/dataModel.d.ts (2)
  • DataModel (60-60)
  • Id (48-49)
lib/config/constants.ts (2)
  • MODEL_DEFAULT (13-13)
  • RECOMMENDED_MODELS (15-27)
convex/rateLimiter.ts (1)
  • rateLimiter (5-41)
convex/users.ts (3)
convex/auth.ts (2)
  • authComponent (21-103)
  • authComponent (144-144)
convex/polar.ts (2)
  • polar (6-31)
  • polar (33-40)
convex/rateLimiter.ts (1)
  • rateLimiter (5-41)
app/auth/page.tsx (1)
lib/auth-client.ts (1)
  • authClient (6-11)
app/providers/convex-client-provider.tsx (1)
lib/auth-client.ts (1)
  • authClient (6-11)
convex/subscription.ts (3)
convex/_generated/server.d.ts (1)
  • MutationCtx (121-121)
convex/_generated/dataModel.d.ts (1)
  • Id (48-49)
convex/rateLimiter.ts (1)
  • rateLimiter (5-41)
convex/chats.ts (2)
lib/error-codes.ts (1)
  • ERROR_CODES (65-73)
convex/_generated/dataModel.d.ts (1)
  • Id (48-49)
app/api/chat/route.ts (1)
lib/auth-server.ts (1)
  • getToken (5-7)
app/layout.tsx (4)
app/providers/convex-client-provider.tsx (1)
  • ConvexClientProvider (24-58)
app/components/auth/auth-guard.tsx (1)
  • AuthGuard (16-44)
app/providers/sidebar-provider.tsx (1)
  • SidebarProvider (13-46)
app/components/layout/layout-app.tsx (1)
  • LayoutApp (9-114)
app/hooks/use-model-settings.ts (1)
lib/config/constants.ts (1)
  • MODEL_DEFAULT (13-13)
app/providers/user-provider.tsx (1)
lib/auth-client.ts (1)
  • authClient (6-11)
convex/http.ts (1)
convex/auth.ts (3)
  • authComponent (21-103)
  • authComponent (144-144)
  • createAuth (105-141)

Comment on lines 27 to 37
// CRITICAL: Check if user already exists by email to preserve existing data
// This is essential for production users with subscriptions and chat history
if (authUser.email) {
const existingUser = await ctx.db
.query("users")
.filter((q) => q.eq(q.field("email"), authUser.email))
.first();

await Promise.all(rateLimitPromises);
} catch (_error) {
// Non-fatal: rate-limit initialisation failure should never block the
// user flow. The rate-limiter will lazily create windows on first use.
}
};

export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
providers: [
Google,
Anonymous({
profile: () => ({ isAnonymous: true }),
}),
],
callbacks: {
async createOrUpdateUser(ctx, args) {
const { existingUserId, type, profile } = args;
if (existingUser) {
// Link Better Auth user to existing user (preserves subscriptions and data!)
await authComponent.setUserId(ctx, authUser._id, existingUser._id);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

🧩 Analysis chain

Use an indexed, case-normalized lookup when linking by email

  • Querying via .filter(q.eq(q.field("email"), ...)).first() will full‑scan unless an index is used. Also, email matching should be case‑insensitive to avoid duplicate accounts (e.g., "User@x.com" vs "user@x.com").

Apply this diff (assumes a by_email_lower index on users(emailLower); if not present, add it in the schema and backfill):

-        if (authUser.email) {
-          const existingUser = await ctx.db
-            .query("users")
-            .filter((q) => q.eq(q.field("email"), authUser.email))
-            .first();
+        const email = authUser.email?.toLowerCase();
+        if (email) {
+          const existingUser = await ctx.db
+            .query("users")
+            .withIndex("by_email_lower", (q) => q.eq("emailLower", email))
+            .first();

If you must keep email (no emailLower yet), at least normalize and add a by_email index:

-        if (authUser.email) {
+        const email = authUser.email?.toLowerCase();
+        if (email) {
           const existingUser = await ctx.db
-            .query("users")
-            .filter((q) => q.eq(q.field("email"), authUser.email))
+            .query("users")
+            .withIndex("by_email", (q) => q.eq("email", email))
             .first();

Run a quick scan to confirm an email index exists and whether the field is emailLower:


🏁 Script executed:

#!/bin/bash
rg -nP 'table\("users"\).*?index.*email' convex/schema -C3

Length of output: 0


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "== convex/auth.ts (first 200 lines) =="
if [ -f convex/auth.ts ]; then
  nl -ba convex/auth.ts | sed -n '1,200p'
else
  echo "convex/auth.ts not found"
fi

echo
echo "== Search repo for email index / emailLower / withIndex / by_email occurrences =="
rg -n --hidden --no-ignore -S \
  -e 'emailLower' -e 'email_lower' -e 'by_email_lower' -e 'by_email' -e 'withIndex(' -e 'with_index(' \
  --glob '!node_modules/**' || true

echo
echo "== Search convex schema dir for users table/index definitions =="
rg -nP 'table\(\"users\"|index.*email|createIndex|index\(' convex -S || true

Length of output: 79


🏁 Script executed:

#!/bin/bash
# Robust re-check for email index / emailLower and auth.ts snippet
echo "== convex/auth.ts (first 200 lines if present) =="
if [ -f convex/auth.ts ]; then
  if command -v sed >/dev/null 2>&1; then
    sed -n '1,200p' convex/auth.ts || true
  else
    head -n 200 convex/auth.ts || true
  fi
else
  echo "convex/auth.ts not found"
fi

echo
echo "== convex/ directory listing =="
if [ -d convex ]; then
  ls -la convex || true
else
  echo "convex directory not found"
fi

echo
echo "== convex/schema files (if any) and grep for email/index =="
if [ -d convex/schema ]; then
  for f in convex/schema/*; do
    echo "---- $f ----"
    if [ -f "$f" ]; then
      if command -v sed >/dev/null 2>&1; then
        sed -n '1,200p' "$f" || true
      else
        head -n 200 "$f" || true
      fi
    fi
    echo
  done
  echo "---- grep for email/index in convex/schema ----"
  if command -v rg >/dev/null 2>&1; then
    rg -n --hidden -S 'email|emailLower|email_lower|index' convex/schema || true
  else
    grep -nR --exclude-dir=node_modules -E 'email|emailLower|email_lower|index' convex/schema || true
  fi
else
  echo "convex/schema not found"
fi

echo
echo "== Repo-wide search for emailLower / by_email / withIndex usages =="
if command -v rg >/dev/null 2>&1; then
  rg -n --hidden -S \
    -e 'emailLower' -e 'email_lower' -e 'by_email_lower' -e 'by_email' \
    -e 'withIndex' -e 'with_index' -e '\\bwithIndex\\b' -e '\\bwith_index\\b' \
    -g '!node_modules/**' || true
else
  echo "rg not available, falling back to grep"
  grep -nR --exclude-dir=node_modules -E 'emailLower|email_lower|by_email_lower|by_email|withIndex|with_index' . || true
fi

echo
echo "== Search convex dir for users table/index patterns =="
if command -v rg >/dev/null 2>&1; then
  rg -n --hidden -S 'table\\(\"users\"|\\bindex\\b.*email|createIndex|index\\(' convex || true
else
  grep -nR --exclude-dir=node_modules -E 'table\\(\"users\"|\\bindex\\b.*email|createIndex|index\\(' convex || true
fi

Length of output: 15466


Use an indexed, case‑normalized lookup when linking users by email

  • Problem: convex/auth.ts (onCreate trigger) performs an unindexed, case‑sensitive filter on authUser.email; convex/schema/user.ts contains email but no lowercased email field or email index was found.
  • Action (required): add a lowercased email field (e.g., emailLower) to convex/schema/user.ts and create an index (e.g., by_email_lower), backfill existing rows, and update the onCreate logic to use authUser.email.toLowerCase() and query by the indexed field. Alternatively, normalize the existing email field (lowercase + index) and backfill—ensure future writes also set the normalized value.
  • Reason: prevents full table scans and avoids duplicate accounts due to case differences.

Comment on lines +12 to +17
await ctx.db.patch(user._id, {
name: undefined, // Better Auth manages this
image: undefined, // Better Auth manages this
isAnonymous: undefined, // Better Auth manages this
emailVerificationTime: undefined, // Better Auth manages this
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

🧩 Analysis chain

Sanity check field removals.

Verify no server/client code still reads these fields from our users table post‑migration.


🏁 Script executed:

#!/bin/bash
rg -n --hidden -S '\b(users\.)?\b(name|image|isAnonymous|emailVerificationTime)\b' -g '!node_modules/**'

Length of output: 148


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "pwd: $(pwd)"
echo "Top-level repo files:"
ls -la | sed -n '1,200p' || true

echo
echo "Searching for direct property accesses on user/users (more targeted):"
rg -n -S -uu -C3 '\b(user\.name|user\.image|user\.isAnonymous|user\.emailVerificationTime|users\.(name|image|isAnonymous|emailVerificationTime))\b' || true

echo
echo "Broader search for these field names (may be noisy) excluding node_modules:"
rg -n -S -uu -C2 '\b(name|image|isAnonymous|emailVerificationTime)\b' -g '!node_modules/**' || true

Length of output: 7782


Hold migration — these user fields are still read by the app.

Found direct reads of user.name and user.isAnonymous in these files (sample results): lib/prompt_config.ts (≈462–463), convex/polar.ts (16–17), convex/email.ts (127,137), app/providers/posthog-provider.tsx (26–28), app/providers/user-provider.tsx (176–177), components/common/upgrade-button.tsx (16), app/tasks/page.tsx (18–19), app/auth/callback/page.tsx (67), app/share/[chatId]/share-view.tsx (56), app/components/layout/chat-sidebar.tsx (88–89), app/components/layout/settings/settings-sidebar.tsx (51), agent_rules/convex_rules.md (622). No references to image or emailVerificationTime were found in this scan.

Action: revert/hold the migration or update all usages to rely on Better Auth/preferredName (or handle undefined) before unsetting these fields.

🤖 Prompt for AI Agents
In convex/migrations/cleanUserData.ts around lines 12–17 the migration unsets
user.name and user.isAnonymous (and also image/emailVerificationTime), but many
app files still read name and isAnonymous; either revert/hold this migration
(restore those fields and remove the patch) OR update all call sites to use
Better Auth/preferredName (or null-safe access) and to handle undefined for
isAnonymous before running the migration; specifically: revert the unset in this
migration for now, or if you proceed with the migration first, update every
usage to fall back to preferredName or check for undefined and update
tests/consumers accordingly.

Comment on lines +4 to +6
// Temporarily keep email for migration - allows onCreate trigger to find existing users
email: v.optional(v.string()),
// TEMPORARY: Keep these fields as optional for migration - will be removed after cleanup
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

🧩 Analysis chain

Normalize + index email for migration lookups; consider a dedicated normalized field

Lowercase/trim email on write to avoid case/whitespace mismatches during backfill and linking. Add a temporary normalized field (plus an index in the table schema) to keep migrations deterministic and fast. Also add a TODO with a removal date.

Apply within this block:

   // Temporarily keep email for migration - allows onCreate trigger to find existing users
   email: v.optional(v.string()),
+  // Normalized, lowercased copy for lookup-only during migration (remove after cleanup)
+  emailNormalized: v.optional(v.string()),
+  // TODO(2025-10-15): Drop both email/emailNormalized after Better Auth cutover is fully verified

If your table definition is modular, add an index (example, adjust to your schema file):

// in convex/schema/index.ts (or wherever defineSchema lives)
import { defineSchema, defineTable } from "convex/server";
import { User } from "./user";

export default defineSchema({
  users: defineTable(User)
    .index("by_emailNormalized", ["emailNormalized"]), // lookup-only
});

To verify normalization/index usage:


🏁 Script executed:

#!/bin/bash
# Find creation/link triggers and email lookups
rg -n -S -g 'convex/**' -e 'onCreate' -e 'onUserCreated' -e 'onAccountLink' -e '\.email\b' -C2
# Find user table definition and indexes
rg -n -S -g 'convex/**' -e 'defineTable\\(.*users' -C3

Length of output: 5053


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "=== ls convex ==="
ls -la convex || true
echo
echo "=== show convex/schema/user.ts (first 200 lines) ==="
sed -n '1,200p' convex/schema/user.ts || true
echo
echo "=== rg: defineTable occurrences ==="
rg -n -S -g 'convex/**' 'defineTable' -C3 || true
echo
echo "=== rg: defineSchema occurrences ==="
rg -n -S -g 'convex/**' 'defineSchema' -C3 || true
echo
echo "=== rg: .index( occurrences ==="
rg -n -S -g 'convex/**' '\.index\(' -C3 || true
echo
echo "=== rg: q.field(\"email\") occurrences ==="
rg -n -S -g 'convex/**' 'q.field\("email"\)' -C3 || true
echo
echo "=== rg: users: keys in schema/defineSchema ==="
rg -n -S -g 'convex/**' -e 'users:' -e '"users"' -C3 || true
echo
echo "=== rg: emailNormalized occurrences ==="
rg -n -S -g 'convex/**' 'emailNormalized' -C3 || true

Length of output: 43825


Add a normalized email field + index and use it for auth lookups

Lowercase+trim email on write and add a temporary lookup-only normalized field with an index for deterministic migration/merge behavior; keep the existing "email" index while migrating and add a TODO removal date.

Apply inside convex/schema/user.ts:

   // Temporarily keep email for migration - allows onCreate trigger to find existing users
   email: v.optional(v.string()),
+  // Normalized, lowercased copy for lookup-only during migration (remove after cleanup)
+  emailNormalized: v.optional(v.string()),
+  // TODO(2025-10-15): Drop both email/emailNormalized after Better Auth cutover is fully verified

Update convex/schema.ts to add an index (keep the existing index on "email"):

users: defineTable(User)
  .index("email", ["email"])
  .index("by_emailNormalized", ["emailNormalized"]),

Update convex/auth.ts onCreate lookup to compare normalized values (trim + toLowerCase) and to use the new indexed field, and ensure any inserts/patches/migrations populate emailNormalized:

// Replace q.field("email") lookup with normalized lookup
const normalized = authUser.email.trim().toLowerCase();
const existingUser = await ctx.db
  .query("users")
  .filter((q) => q.eq(q.field("emailNormalized"), normalized))
  .first();

Add a one-off migration (or extend existing convex/migrations/* scripts) to populate emailNormalized for all existing users from user.email (user.email?.trim().toLowerCase()) before switching lookups, and ensure any code paths that write email also write the normalized copy.

🤖 Prompt for AI Agents
In convex/schema/user.ts around lines 4-6, add a new optional field
emailNormalized and a TODO with a removal date, and ensure writes populate
emailNormalized as the trimmed lowercase of email; in convex/schema.ts add a new
index "by_emailNormalized" on ["emailNormalized"] while keeping the existing
"email" index; update convex/auth.ts onCreate lookup to compute normalized =
authUser.email.trim().toLowerCase() and query users by emailNormalized instead
of email; add a one-off migration (or extend existing migrations) to populate
emailNormalized for all existing users from user.email?.trim().toLowerCase(),
and audit/patch any code paths that insert or update email to also set the
normalized copy.

…eanup

Migrated from manual deletion mutation to Better Auth's built-in user deletion
with enhanced onDelete trigger for comprehensive data cleanup.

Changes:
- Enable Better Auth deleteUser feature in auth configuration
- Move all cleanup logic to onDelete trigger in convex/auth.ts
- Add cleanup for previously missed data: API keys, connectors, scheduled tasks
- Update settings page to use authClient.deleteUser() instead of mutation
- Remove manual deleteAccount mutation from convex/users.ts
- Maintain concurrent deletion patterns and R2 file cleanup

Benefits:
- Built-in password verification and session management
- Automatic session invalidation on deletion
- Single source of truth for deletion logic in onDelete trigger
- Comprehensive cleanup of ALL user data including previously missed tables
- Framework-compliant user deletion flow

Fixes comment about Better Auth user deletion not triggering onDelete.
…cement

Add ARIA role="alert" to the error message in anonymous sign-in component
to ensure screen readers properly announce error states to users.

- Meets WCAG 2.1 AA 4.1.3 Status Messages criterion
- Creates live region for immediate announcement
- Improves accessibility for assistive technology users
- Add missing indexes: by_originalChatId and by_user_and_status
- Replace .filter() calls with .withIndex() for efficient queries
- Remove unused userId parameters from helper functions
- Optimize scheduled task queries with proper status indexing
- Improve chat branching queries with dedicated originalChatId index

Fixes Convex rule violations per agent_rules/convex_rules.md
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (1)
convex/scheduled_tasks.ts (1)

377-404: Bug: rescheduling fails when status and schedule change in the same request

If task.status !== "active" and args.status becomes "active" while schedule fields also change, no reschedule occurs and nextExecution/scheduledFunctionId remain unset.

-      // Only reschedule if task status is active (or becoming active)
-      if (
-        args.status !== "paused" &&
-        args.status !== "archived" &&
-        task.status === "active"
-      ) {
+      // Determine final status after this update
+      const finalStatus = args.status ?? task.status;
+      // Only reschedule when final status is active
+      if (finalStatus === "active") {
         const nextExecution = calculateNextExecution(
           newScheduleType,
           newScheduledTime,
           args.timezone ?? task.timezone,
           newScheduledDate
         );
         const scheduledFunctionId = await ctx.scheduler.runAt(
           nextExecution,
           internal.scheduled_ai.executeTask,
           { taskId: args.taskId }
         );
         updates.scheduledTime = newScheduledTime;
         updates.nextExecution = nextExecution;
         updates.scheduledFunctionId = scheduledFunctionId;
       } else {
         updates.scheduledTime = newScheduledTime;
         updates.scheduledFunctionId = undefined;
         updates.nextExecution = undefined;
       }

Optionally, guard the later "status transitions" block to skip duplicate scheduling when schedule fields already handled it.

Also applies to: 406-444

🧹 Nitpick comments (7)
app/settings/page.tsx (2)

223-231: Add explicit button types for a11y and to avoid implicit submit behavior

Per guidelines, add type="button" to non-form buttons (Danger Zone trigger and dialog actions).

Example (apply similarly to other buttons here):

<Button type="button" disabled={isDeleting} onClick={handleDeleteAccount} size="sm" variant="destructive"></Button>

<Button type="button" onClick={() => setShowDeleteAccountDialog(false)} variant="outline">Cancel</Button>
<Button type="button" onClick={confirmDeleteAccount} variant="destructive">Delete Account</Button>

Also applies to: 255-257


154-167: Align Phosphor icon naming with project convention

Guidelines call for Icon-suffixed components. Alias on import to comply without refactors elsewhere.

Example:

import { CircleNotch as CircleNotchIcon, Headset as HeadsetIcon, Rocket as RocketIcon, Sparkle as SparkleIcon } from "@phosphor-icons/react";

// usage
<RocketIcon className="…" aria-hidden />
<SparkleIcon className="…" aria-hidden />
<HeadsetIcon className="…" aria-hidden />
<CircleNotchIcon className="…" />

Also mark decorative icons aria-hidden to reduce verbosity for screen readers.

Also applies to: 183-183, 225-225

app/api/create-chat/route.ts (1)

28-28: Add an explicit type for token to satisfy strict TS guidelines.

Prevents accidental any if getToken typing changes.

-    const token = await getToken();
+    const token: string | null = await getToken();
app/components/auth/anonymous-sign-in.tsx (2)

28-34: Prefer in‑app retry over full page reload; add visible focus styles

A full reload heavy‑hands the UX. Offer an in-place retry and ensure a visible focus style per our a11y guideline.

Apply this minimal change here:

-        <button
-          className="rounded-md bg-primary px-4 py-2 text-primary-foreground"
-          onClick={() => window.location.reload()}
+        <button
+          className="rounded-md bg-primary px-4 py-2 text-primary-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 ring-offset-background"
+          onClick={handleRetry}
           type="button"
         >
-          Refresh Page
+          Try again
         </button>

And add this helper inside the component:

const handleRetry = async () => {
  setError(null);
  try {
    await authClient.signIn.anonymous();
  } catch {
    setError("Failed to sign in. Please try again.");
  }
};

15-19: Guard anonymous sign‑in by auth state; add unmount safety and avoid floating promise.

Call signIn only when unauthenticated, protect setError from running after unmount, and prefix the promise with void to avoid a floating promise.

 useEffect(() => {
+  let cancelled = false;
   if (!attemptedAnon.current) {
     attemptedAnon.current = true;
-    authClient.signIn.anonymous().catch(() => {
-      setError("Failed to sign in. Please refresh the page.");
-    });
+    void authClient.signIn.anonymous().catch(() => {
+      if (!cancelled) setError("Failed to sign in. Please refresh the page.");
+    });
   }
-}, []);
+  return () => {
+    cancelled = true;
+  };
+}, []);

Ensure this component only mounts for unauthenticated users (e.g., route guard or useSession()/status === "unauthenticated").

convex/chats.ts (1)

412-414: Optional: update updatedAt when toggling pin

Keeps sorting/freshness consistent with other mutations.

     await ctx.db.patch(chatId, {
       isPinned: newPinnedStatus,
+      updatedAt: Date.now(),
     });
convex/lib/cleanup_helper.ts (1)

48-60: Optional: bound R2 deletes to reduce throttling risk on large chats

Large parallel deletes can hit provider rate limits. Consider chunking.

-  await Promise.all(
-    attachments.map(async (attachment) => {
+  const chunk = <T>(arr: T[], size: number) =>
+    Array.from({ length: Math.ceil(arr.length / size) }, (_, i) =>
+      arr.slice(i * size, (i + 1) * size)
+    );
+  for (const batch of chunk(attachments, 10)) {
+    await Promise.all(batch.map(async (attachment) => {
       try {
         await r2.deleteObject(ctx, attachment.key);
       } catch {
       }
       await ctx.db.delete(attachment._id);
-    })
-  );
+    }));
+  }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a00dfb5 and bcb23cf.

📒 Files selected for processing (11)
  • app/api/chat/route.ts (2 hunks)
  • app/api/create-chat/route.ts (2 hunks)
  • app/components/auth/anonymous-sign-in.tsx (1 hunks)
  • app/settings/page.tsx (3 hunks)
  • convex/auth.ts (1 hunks)
  • convex/chats.ts (6 hunks)
  • convex/lib/cleanup_helper.ts (3 hunks)
  • convex/messages.ts (5 hunks)
  • convex/scheduled_tasks.ts (2 hunks)
  • convex/schema.ts (2 hunks)
  • convex/users.ts (16 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/api/chat/route.ts
  • convex/messages.ts
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use TypeScript with strict mode; avoid any/unknown; prefer explicit types/interfaces; do not use @ts-ignore or disable type checks
Don’t use the TypeScript any type; avoid implicit any; don’t use @ts-ignore; prefer export type/import type; avoid non-null (!) misuse
Use arrow functions over function expressions; use const for single-assignment variables; avoid var
Avoid unnecessary constructs: fragments, boolean casts, string concatenation where template literals suffice, useless this aliasing, unnecessary constructors/continue/catch
Prefer for...of and while loops appropriately; avoid Array.forEach when for...of improves clarity/perf
Ensure no unused imports, variables, params, labels, private members; prevent import cycles
Use Number.isNaN / Number.isFinite; always pass radix to parseInt; use Date.now; prefer object spread over Object.assign; prefer template literals over string concatenation
Disallow console/debugger; use ===/!==; avoid duplicate/sparse arrays, duplicate keys/members/cases, reassignment of imports/natives
React: call hooks at top level; specify hook deps correctly; provide keys; avoid array index keys; don’t define components inside components; don’t use event handlers on non-interactive elements
Don’t hardcode secrets; ensure Promise-like results are handled; avoid await in loops; ensure switch statements are exhaustive and default comes last
Use node: protocol for Node built-ins and prefer node:assert/strict
Use with { type: "json" } for JSON module imports
Animation: honor prefers-reduced-motion; animate transform/opacity; avoid layout-affecting properties
Style: don’t use global eval/with; avoid nested/negative ifs; prefer else-if/early return; use ** over Math.pow; prefer assignment shorthand
Testing Best Practices: avoid focused/disabled tests; ensure expect is inside it(); don’t use export/module.exports in test files
Next.js rule: don’t import next/head in pages/_document.*
Security: don’t assign to document.cookie directl...

Files:

  • convex/scheduled_tasks.ts
  • convex/auth.ts
  • app/components/auth/anonymous-sign-in.tsx
  • convex/lib/cleanup_helper.ts
  • convex/users.ts
  • convex/chats.ts
  • app/api/create-chat/route.ts
  • app/settings/page.tsx
  • convex/schema.ts
convex/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Follow Convex-specific guidelines in agent_rules/convex_rules.md for queries, mutations, actions, and server logic

Files:

  • convex/scheduled_tasks.ts
  • convex/auth.ts
  • convex/lib/cleanup_helper.ts
  • convex/users.ts
  • convex/chats.ts
  • convex/schema.ts
convex/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Organize Convex API functions: queries/mutations/actions/internal under convex/

Files:

  • convex/scheduled_tasks.ts
  • convex/auth.ts
  • convex/lib/cleanup_helper.ts
  • convex/users.ts
  • convex/chats.ts
  • convex/schema.ts
{app,components}/**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

Use functional React components; call hooks only at top level; do not use default exports for components or functions

Files:

  • app/components/auth/anonymous-sign-in.tsx
  • app/settings/page.tsx
**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.tsx: Use Tailwind CSS utility classes for styling and avoid inline styles
Ensure accessibility (semantic HTML, alt text, proper ARIA) per agent_rules/ultracite.md
Use Phosphor React icon components with the Icon suffix (e.g., CaretIcon, not Caret)
Next.js rule: don’t use or elements; use framework-provided components/APIs
A11y: Include meaningful alt text for images; do not include the words “image”, “picture”, or “photo” in alt
A11y: Use semantic elements and valid ARIA roles/states; avoid adding unsupported ARIA to elements
A11y: Buttons must include explicit type attribute
A11y: Provide visible focus, keyboard support, and correct key/mouse event pairing (onClick with keyboard, hover with focus)
A11y: SVGs and iframes must include title; html element must include lang
React/JSX: don’t use dangerous props, don’t mix children with dangerouslySetInnerHTML, avoid extra closing tags, use <> fragments, don’t insert comments as text nodes
Content & a11y: ensure <title> reflects context; icon-only buttons need aria-label; maintain accessible names; add skip links and scroll-margin-top on headings
Performance: virtualize large lists; preload above-the-fold images and lazy-load others; prevent CLS via explicit image dimensions

Files:

  • app/components/auth/anonymous-sign-in.tsx
  • app/settings/page.tsx
🧠 Learnings (13)
📚 Learning: 2025-09-20T14:21:06.336Z
Learnt from: CR
PR: ajanraj/OpenChat#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-20T14:21:06.336Z
Learning: Applies to **/*.tsx : Ensure accessibility (semantic HTML, alt text, proper ARIA) per agent_rules/ultracite.md

Applied to files:

  • app/components/auth/anonymous-sign-in.tsx
📚 Learning: 2025-09-20T14:21:06.336Z
Learnt from: CR
PR: ajanraj/OpenChat#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-20T14:21:06.336Z
Learning: Applies to **/*.tsx : A11y: Use semantic elements and valid ARIA roles/states; avoid adding unsupported ARIA to elements

Applied to files:

  • app/components/auth/anonymous-sign-in.tsx
📚 Learning: 2025-09-20T14:21:06.336Z
Learnt from: CR
PR: ajanraj/OpenChat#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-20T14:21:06.336Z
Learning: Applies to **/*.tsx : Content & a11y: ensure <title> reflects context; icon-only buttons need aria-label; maintain accessible names; add skip links and scroll-margin-top on headings

Applied to files:

  • app/components/auth/anonymous-sign-in.tsx
📚 Learning: 2025-09-20T14:21:06.336Z
Learnt from: CR
PR: ajanraj/OpenChat#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-20T14:21:06.336Z
Learning: Applies to **/*.tsx : A11y: SVGs and iframes must include title; html element must include lang

Applied to files:

  • app/components/auth/anonymous-sign-in.tsx
📚 Learning: 2025-09-20T14:21:06.336Z
Learnt from: CR
PR: ajanraj/OpenChat#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-20T14:21:06.336Z
Learning: Applies to **/*.tsx : A11y: Buttons must include explicit type attribute

Applied to files:

  • app/components/auth/anonymous-sign-in.tsx
📚 Learning: 2025-09-20T14:21:06.336Z
Learnt from: CR
PR: ajanraj/OpenChat#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-20T14:21:06.336Z
Learning: Applies to **/*.tsx : A11y: Provide visible focus, keyboard support, and correct key/mouse event pairing (onClick with keyboard, hover with focus)

Applied to files:

  • app/components/auth/anonymous-sign-in.tsx
📚 Learning: 2025-09-20T14:21:06.336Z
Learnt from: CR
PR: ajanraj/OpenChat#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-20T14:21:06.336Z
Learning: Applies to **/*.tsx : A11y: Include meaningful alt text for images; do not include the words “image”, “picture”, or “photo” in alt

Applied to files:

  • app/components/auth/anonymous-sign-in.tsx
📚 Learning: 2025-08-31T22:44:58.866Z
Learnt from: ajanraj
PR: ajanraj/OpenChat#15
File: convex/messages.ts:79-82
Timestamp: 2025-08-31T22:44:58.866Z
Learning: In the R2 storage migration codebase, the saveFileAttachment function in convex/files.ts always sets the chatId field when creating chat_attachment records, so attachments referenced in messages will always have chatId populated. The cleanup logic in convex/messages.ts correctly uses the strict check attByKey.chatId === chatId to prevent accidental deletion of attachments from other chats.

Applied to files:

  • convex/lib/cleanup_helper.ts
📚 Learning: 2025-08-31T22:44:58.866Z
Learnt from: ajanraj
PR: ajanraj/OpenChat#15
File: convex/messages.ts:79-82
Timestamp: 2025-08-31T22:44:58.866Z
Learning: In the OpenChat R2 storage migration, attachments follow a two-phase creation pattern: first inserted as pending rows with only userId and key, then later patched with chatId and full metadata via internalSave. The cleanup logic in convex/messages.ts correctly uses the strict check attByKey.chatId === chatId because attachments referenced in messages will always have chatId populated after the patching phase.

Applied to files:

  • convex/lib/cleanup_helper.ts
  • convex/chats.ts
  • convex/schema.ts
📚 Learning: 2025-08-31T22:44:58.866Z
Learnt from: ajanraj
PR: ajanraj/OpenChat#15
File: convex/messages.ts:79-82
Timestamp: 2025-08-31T22:44:58.866Z
Learning: In the OpenChat R2 storage migration, attachments follow a two-phase creation pattern in convex/files.ts: first inserted as pending rows with only userId and key (line 42-45), then later patched with chatId and full metadata via internalSave function during saveFileAttachment. The cleanup logic in convex/messages.ts correctly uses the strict check attByKey.chatId === chatId because attachments referenced in messages will always have chatId populated after the patching phase.

Applied to files:

  • convex/lib/cleanup_helper.ts
📚 Learning: 2025-09-20T14:21:06.336Z
Learnt from: CR
PR: ajanraj/OpenChat#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-20T14:21:06.336Z
Learning: Applies to convex/**/*.{ts,tsx} : Follow Convex-specific guidelines in agent_rules/convex_rules.md for queries, mutations, actions, and server logic

Applied to files:

  • convex/users.ts
  • convex/chats.ts
📚 Learning: 2025-09-20T14:21:06.336Z
Learnt from: CR
PR: ajanraj/OpenChat#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-20T14:21:06.336Z
Learning: Applies to convex/**/*.ts : Organize Convex API functions: queries/mutations/actions/internal under convex/

Applied to files:

  • convex/users.ts
📚 Learning: 2025-09-20T14:21:06.336Z
Learnt from: CR
PR: ajanraj/OpenChat#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-20T14:21:06.336Z
Learning: Applies to convex/schema/**/*.ts : Define Convex database schema in convex/schema/ with modular tables

Applied to files:

  • convex/schema.ts
🧬 Code graph analysis (8)
convex/auth.ts (4)
convex/_generated/dataModel.d.ts (2)
  • DataModel (60-60)
  • Id (48-49)
lib/config/constants.ts (2)
  • MODEL_DEFAULT (13-13)
  • RECOMMENDED_MODELS (15-27)
convex/rateLimiter.ts (1)
  • rateLimiter (5-41)
convex/files.ts (1)
  • r2 (17-71)
app/components/auth/anonymous-sign-in.tsx (1)
lib/auth-client.ts (1)
  • authClient (6-11)
convex/lib/cleanup_helper.ts (2)
convex/_generated/server.d.ts (1)
  • MutationCtx (121-121)
convex/_generated/dataModel.d.ts (2)
  • Id (48-49)
  • Doc (30-33)
convex/users.ts (2)
convex/auth.ts (2)
  • authComponent (22-192)
  • authComponent (238-238)
convex/polar.ts (2)
  • polar (6-31)
  • polar (33-40)
convex/chats.ts (2)
convex/auth.ts (2)
  • authComponent (22-192)
  • authComponent (238-238)
convex/lib/cleanup_helper.ts (2)
  • deleteChatCompletely (100-115)
  • deleteMultipleChats (124-144)
app/api/create-chat/route.ts (2)
lib/auth-server.ts (1)
  • getToken (5-7)
lib/error-utils.ts (1)
  • createErrorResponse (280-295)
app/settings/page.tsx (2)
app/providers/user-provider.tsx (1)
  • useUser (252-258)
lib/auth-client.ts (1)
  • authClient (6-11)
convex/schema.ts (1)
convex/schema/chat.ts (1)
  • Chat (3-15)
🔇 Additional comments (18)
app/settings/page.tsx (3)

4-4: LGTM: switched to Convex useAction appropriately

Using actions for checkout/portal generation fits the flow here.


20-20: LGTM: centralized Better Auth client import

Importing from lib/auth-client keeps auth calls consistent.


29-29: LGTM: minimal user context usage

hasPremium and products suffice for this page.

app/api/create-chat/route.ts (2)

37-37: Comment-only change. No action needed.


5-5: Auth token import is correct — Convex plugin enabled.

lib/auth-server.ts imports getTokenNextjs from @convex-dev/better-auth/nextjs and calls getTokenNextjs(createAuth); convex/auth.ts configures betterAuth with plugins including convex(), so getToken returns a Convex-compatible auth token usable by fetchQuery/fetchMutation.

app/components/auth/anonymous-sign-in.tsx (3)

9-9: Typed error state looks good

Explicit string | null keeps strict TS happy.


25-27: A11y: Status message now announced

role="alert" resolves the earlier accessibility comment (WCAG 4.1.3). ✅


3-5: Confirm authClient is safe for client import

lib/auth-client.ts only imports client-side packages (@convex-dev/better-auth/client/plugins, better-auth/client/plugins, better-auth/react); no server-only imports (next/headers, node:, fs, path, etc.) were found.

convex/users.ts (3)

48-51: Coalesce Better Auth nullable fields to undefined to satisfy Convex validators

Better Auth may return null for name/email; User.fields likely uses v.optional(v.string()). Return undefined, not null, to avoid runtime validation errors.

-      name: authUser.name,
-      email: authUser.email,
+      name: authUser.name ?? undefined,
+      email: authUser.email ?? undefined,

32-40: LGTM: switched to Better Auth and null-guarded access

Safe use of authComponent.safeGetAuthUser(ctx) and early null returns look good.


98-101: API contract analysis reveals no client-side null handling for affected mutations—changes are breaking

After examining the codebase, the original concern is validated. The affected mutations (updateUserProfile, toggleFavoriteModel, setModelEnabled, bulkSetModelsDisabled, bulkSetFavoriteModels, incrementMessageCount, assertNotOverLimit) are called from client hooks and API routes that do not handle null returns but expect exceptions on authentication failures.

Key findings:

  • Client hooks (use-model-preferences.ts, use-model-settings.ts) wrap calls in try-catch expecting errors, not null checks
  • API routes (chat/route.ts, create-chat/route.ts) rely on exception handling for authentication failures
  • The getCurrentUser query correctly returns null for unauthenticated users, but mutations now throw instead
  • Return type v.null() contradicts the throwing behavior

The mutations should either:

  1. Preserve null returns to maintain backward compatibility with existing error handling patterns
  2. Update return types to remove v.null() if throwing is the intended behavior

Current implementation creates inconsistency between declared return types and runtime behavior.

convex/scheduled_tasks.ts (1)

186-188: LGTM: composite index by_user_and_status for active-task queries

Good perf/accuracy improvement over filtering in memory.

Also applies to: 601-603

convex/schema.ts (1)

16-18: LGTM: indices added for new access patterns

by_originalChatId, chat_attachments.by_key, and scheduled_tasks.by_user_and_status align with query changes.

Also applies to: 29-29, 42-42

convex/chats.ts (1)

6-6: LGTM: Better Auth integration and cleanup helper usage

Auth migration (safeGetAuthUser + userId casting) and simplified deletions via helpers look correct.

Also applies to: 95-100, 205-210, 268-268, 289-289, 307-307

convex/lib/cleanup_helper.ts (1)

72-80: LGTM: remove userId param and rely on by_originalChatId

Signature simplification + correct index usage; bulk path aligns with callers.

Also applies to: 102-106, 126-131

convex/auth.ts (3)

187-189: Make final user delete idempotent

If the user doc was pre-deleted, ctx.db.delete will throw. Check existence or ignore not-found.

-        // Finally delete user record
-        await ctx.db.delete(userId);
+        // Finally delete user record (idempotent)
+        const existing = await ctx.db.get(userId);
+        if (existing) {
+          await ctx.db.delete(userId);
+        }

207-210: Fail fast on missing Google OAuth env vars

Empty strings hide misconfig; throw early with a clear message.

-      google: {
-        clientId: process.env.AUTH_GOOGLE_ID || "",
-        clientSecret: process.env.AUTH_GOOGLE_SECRET || "",
-      },
+      google: {
+        clientId:
+          process.env.AUTH_GOOGLE_ID ??
+          (() => {
+            throw new Error("AUTH_GOOGLE_ID is required");
+          })(),
+        clientSecret:
+          process.env.AUTH_GOOGLE_SECRET ??
+          (() => {
+            throw new Error("AUTH_GOOGLE_SECRET is required");
+          })(),
+      },

28-35: Normalize email lookups to lowercase (or query a dedicated lowercased field + index).

Lowercase authUser.email for lookups to avoid duplicates; if the DB stores mixed-case emails add emailLower + index and query that instead. Location: convex/auth.ts (lines ~28–41). Repository search found no emailLower/by_email_lower—confirm schema/index.

-        if (authUser.email) {
-          const existingUser = await ctx.db
-            .query("users")
-            .withIndex("email", (q) => q.eq("email", authUser.email))
-            .first();
+        const email = authUser.email?.toLowerCase();
+        if (email) {
+          const existingUser = await ctx.db
+            .query("users")
+            .withIndex("email", (q) => q.eq("email", email))
+            .first();

If emails aren’t stored lowercased, switch to a dedicated field/index:

.withIndex("by_email_lower", (q) => q.eq("emailLower", email))

Comment on lines +31 to +33
if (!token) {
return createErrorResponse(new Error("Unauthorized"));
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

🧩 Analysis chain

Return explicit 401 instead of relying on Error("Unauthorized") message classification.

Safer and clearer. If you prefer keeping createErrorResponse, consider introducing a dedicated UnauthorizedError (or helper) that your classifier maps to 401.

Option A (explicit response here):

-      return createErrorResponse(new Error("Unauthorized"));
+      return new Response(
+        JSON.stringify({ error: { type: "Unauthorized", code: "UNAUTHORIZED" } }),
+        {
+          status: 401,
+          headers: {
+            "Content-Type": "application/json",
+            "WWW-Authenticate": 'Bearer realm="OpenChat", error="invalid_token"',
+          },
+        }
+      );

Option B (keep central error formatting; add a typed error):

  • In lib/error-utils.ts, add a tiny helper/type your classifier recognizes (example):
export class UnauthorizedError extends Error {
  readonly code = "UNAUTHORIZED";
  constructor(message = "Unauthorized") {
    super(message);
    this.name = "UnauthorizedError";
  }
}
export const unauthorized = (msg?: string) => new UnauthorizedError(msg);
  • Then change here to:
return createErrorResponse(unauthorized());

Verify your classifier maps this to 401:


🏁 Script executed:

#!/bin/bash
rg -nC5 -S 'classifyError\\s*\\(' lib/error-utils.ts

Length of output: 90


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== rg createErrorResponse =="
rg -n -S -C3 'createErrorResponse' || true

echo "== rg classifyError =="
rg -n -S -C3 'classifyError' || true

echo "== rg UnauthorizedError =="
rg -n -S -C3 'UnauthorizedError' || true

echo "== rg Unauthorized =="
rg -n -S -C3 'Unauthorized' || true

echo "== file: app/api/create-chat/route.ts =="
if [ -f app/api/create-chat/route.ts ]; then sed -n '1,240p' app/api/create-chat/route.ts; else echo "MISSING: app/api/create-chat/route.ts"; fi

echo "== file: lib/error-utils.ts =="
if [ -f lib/error-utils.ts ]; then sed -n '1,240p' lib/error-utils.ts; else echo "MISSING: lib/error-utils.ts"; fi

Length of output: 29256


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== lib/error-utils.ts (full) =="
sed -n '1,400p' lib/error-utils.ts || true

echo "== grep for 'unauthor' (case-insensitive) =="
rg -n --hidden -S -i 'unauthor' || true

echo "== grep for 'NotAuthenticated' / 'NOT_AUTHENTICATED' =="
rg -n --hidden -S 'NOT_AUTHENTICATED|NOT_AUTHENTICATED' || true

echo "== show createErrorResponse function region =="
rg -n --hidden -n 'export function createErrorResponse' -C5 lib/error-utils.ts || true
sed -n '270,340p' lib/error-utils.ts || true

echo "== show classifyError function region =="
rg -n --hidden -n 'export function classifyError' -C2 lib/error-utils.ts || true
sed -n '200,280p' lib/error-utils.ts || true

Length of output: 20668


Return explicit 401 for unauthenticated cases (don't rely on Error("Unauthorized"))

classifyError treats plain Error instances as SYSTEM_ERROR (httpStatus 500), so createErrorResponse(new Error("Unauthorized")) currently returns 500. Fix the unauthorized returns in app/api/create-chat/route.ts (lines ~31–33 and ~37–40) — change to either:

Option A (explicit 401):

-      return createErrorResponse(new Error("Unauthorized"));
+      return new Response(
+        JSON.stringify({ error: { type: "Unauthorized", code: "UNAUTHORIZED" } }),
+        {
+          status: 401,
+          headers: {
+            "Content-Type": "application/json",
+            "WWW-Authenticate": 'Bearer realm="OpenChat", error="invalid_token"',
+          },
+        }
+      );

Option B (keep central error formatting; add a typed error):

  • Add a tiny helper/typed error the classifier recognizes (e.g., unauthorized() or UnauthorizedError that maps to ERROR_CODES.UNAUTHORIZED) and replace with:
return createErrorResponse(unauthorized());

Also update other endpoints that currently use createErrorResponse(new Error("Unauthorized")) (e.g., app/api/chat/route.ts, app/api/rate-limits/route.ts).

🤖 Prompt for AI Agents
In app/api/create-chat/route.ts around lines 31–33 and 37–40, the code returns
createErrorResponse(new Error("Unauthorized")) which the classifier maps to 500;
replace these with a proper 401 response by using the typed unauthorized error
helper (e.g., import and return createErrorResponse(unauthorized())) or
explicitly return a 401 response object, and add the tiny
UnauthorizedError/unauthorized() helper that the classifier recognizes (mapped
to ERROR_CODES.UNAUTHORIZED) if it doesn't exist; also update the other
endpoints that currently call createErrorResponse(new Error("Unauthorized"))
(app/api/chat/route.ts, app/api/rate-limits/route.ts) to use the same
unauthorized() helper or an explicit 401 so unauthenticated cases return HTTP
401.

Comment on lines 76 to 84
const confirmDeleteAccount = useCallback(async () => {
setIsDeleting(true);
try {
await deleteAccount({});
await signOut();
// Use Better Auth's user deletion which will trigger the onDelete trigger
// This handles comprehensive cleanup automatically
await authClient.deleteUser();

// No need to manually sign out as Better Auth handles session cleanup
toast({ title: "Account deleted", status: "success" });
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Keep the dialog open on failure and prevent double-submits

Currently the dialog always closes in finally, so users can’t retry after an error. Also, the confirm button isn’t disabled during deletion, allowing rapid re-clicks.

Apply this diff to keep the dialog open on error:

   } catch (_error) {
     toast({
       title: "Failed to delete account",
       description:
         "Please try again or contact support if the problem persists.",
       status: "error",
     });
   } finally {
-    setIsDeleting(false);
-    setShowDeleteAccountDialog(false);
+    setIsDeleting(false);
   }

And update the confirm button to block duplicate requests and show progress (outside the changed hunk):

<Button onClick={confirmDeleteAccount} variant="destructive" disabled={isDeleting}>
  {isDeleting ? (
    <>
      <CircleNotch className="mr-2 h-4 w-4 animate-spin" />
      Deleting...
    </>
  ) : (
    "Delete Account"
  )}
</Button>

Also applies to: 86-97

🤖 Prompt for AI Agents
In app/settings/page.tsx around lines 76-84 (and also apply to 86-97), the
deletion dialog currently always closes in a finally block and the confirm
button allows duplicate submits; change confirmDeleteAccount so it sets
isDeleting(true) before the async call, await authClient.deleteUser() inside
try, on success setIsDeleting(false), close the dialog and show the success
toast, and in catch setIsDeleting(false) and show the error toast but do NOT
close the dialog so the user can retry; remove any finally that always closes
the dialog. Also update the confirm button (outside the changed hunk) to disable
when isDeleting is true and render a spinner + "Deleting..." when in progress to
prevent double-submits.

Comment on lines 14 to +15
users: defineTable(User).index("email", ["email"]),
chats: defineTable(Chat).index("by_user", ["userId"]),
chats: defineTable(Chat)
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

🧩 Analysis chain

Normalize and index emails for O(1) case-insensitive linking

auth.ts links users by email; to avoid duplicate accounts (case variants) and scans, normalize and index a lowercased field.

 export default defineSchema({
-  users: defineTable(User).index("email", ["email"]),
+  users: defineTable(User)
+    .index("email", ["email"])
+    // Requires adding `emailLower` to User schema and backfilling existing rows
+    .index("by_email_lower", ["emailLower"]),

Verify whether emailLower exists and is populated:


🏁 Script executed:

#!/bin/bash
rg -n -S '\bemailLower\b|by_email_lower' convex --hidden --no-ignore

Length of output: 0


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Searching for occurrences of emailLower or by_email_lower..."
rg -n -S '\bemailLower\b|by_email_lower' --hidden --no-ignore -g '!*node_modules*' || true

echo
echo "Searching for index(\"email\") and related index uses..."
rg -n -S 'index\(\s*"email"' --hidden --no-ignore -g '!*node_modules*' -C2 || true
rg -n -S 'index\(\s*"by_email_lower"' --hidden --no-ignore -g '!*node_modules*' -C2 || true

echo
echo "Searching for schema.ts files (fd fallback to find)..."
fd -t f 'schema.ts' || true
find . -type f -name 'schema.ts' -print || true

echo
echo "Show convex/schema.ts (if present, first 200 lines)..."
if [ -f convex/schema.ts ]; then
  sed -n '1,200p' convex/schema.ts
else
  echo "convex/schema.ts not found"
fi

echo
echo "Search for defineSchema and users: defineTable occurrences..."
rg -n --hidden --no-ignore 'defineSchema' -S -C2 || true
rg -n -S 'users\s*:\s*defineTable' --hidden --no-ignore -g '!*node_modules*' -C3 || true

echo
echo "Search for User type/definition (interface|type|const)..."
rg -n -S '\binterface\s+User\b|\btype\s+User\b|\bconst\s+User\b' --hidden --no-ignore -g '!*node_modules*' -C3 || true

echo
echo "List convex directory (if exists)..."
ls -la convex || true

Length of output: 6236


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Listing convex/schema directory:"
ls -la convex/schema || true

echo
echo "Displaying convex/schema/user.* if present (first 300 lines):"
for f in convex/schema/user.*; do
  if [ -f "$f" ]; then
    echo "---- $f ----"
    sed -n '1,300p' "$f"
  fi
done

echo
echo "Displaying convex/auth.ts (first 400 lines):"
if [ -f convex/auth.ts ]; then
  sed -n '1,400p' convex/auth.ts
else
  echo "convex/auth.ts not found"
fi

echo
echo "Search for any occurrences of emailLower or by_email_lower under convex:"
rg -n --hidden 'emailLower|by_email_lower' convex || true

echo
echo "Search for toLowerCase usage under convex:"
rg -n --hidden 'toLowerCase\(' convex || true

echo
echo "Search for index(\"email\") usages under convex:"
rg -n --hidden 'index\(\s*\"email\"' convex -C3 || true

Length of output: 9942


Add lowercased email (emailLower), backfill it, add a by_email_lower index, and query by it

auth.onCreate currently links by the case-sensitive "email" index; the schema has no emailLower or by_email_lower index. Action items (minimal):

  • Add emailLower: v.optional(v.string()) to convex/schema/user.ts and set it whenever email is written (emailLower = email?.toLowerCase()).
  • Backfill existing users via a migration (populate emailLower from email).
  • Add the index in convex/schema.ts (keep the existing "email" index during migration):
 export default defineSchema({
-  users: defineTable(User).index("email", ["email"]),
+  users: defineTable(User)
+    .index("email", ["email"])
+    // Requires adding `emailLower` to User schema and backfilling existing rows
+    .index("by_email_lower", ["emailLower"]),
  • Update convex/auth.ts onCreate to link using the new index and a lowercased value (e.g., use .withIndex("by_email_lower", q => q.eq("emailLower", authUser.email.toLowerCase()))). Remove the old index after migration/cleanup.

Files to change: convex/schema/user.ts, convex/schema.ts (users entry), convex/auth.ts (onCreate).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
users: defineTable(User).index("email", ["email"]),
chats: defineTable(Chat).index("by_user", ["userId"]),
chats: defineTable(Chat)
export default defineSchema({
users: defineTable(User)
.index("email", ["email"])
// Requires adding `emailLower` to User schema and backfilling existing rows
.index("by_email_lower", ["emailLower"]),
chats: defineTable(Chat)
});

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