-
-
Notifications
You must be signed in to change notification settings - Fork 21
feat: migrate from Convex Auth to Better Auth #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
- 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.
The latest updates on your projects. Learn more about Vercel for GitHub. |
Note Other AI code review bot(s) detectedCodeRabbit 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. WalkthroughProject 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
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested labels
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this 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 "authenticatedDaily" may leave "anonymousDaily" 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="alert" (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(); |
There was a problem hiding this comment.
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) => {
+ // Get all users
+ const allUsers = await ctx.db.query("users").collect();
+
+ // Update each user to remove duplicate fields that Better Auth now manages
</file context>
return null; | ||
const authUser = await authComponent.safeGetAuthUser(ctx); | ||
if (!authUser?.userId) { | ||
throw new ConvexError(ERROR_CODES.NOT_AUTHENTICATED); |
There was a problem hiding this comment.
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
There was a problem hiding this 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 residencyYou’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.
Moveapp.use(betterAuth)
to the top of convex/convex.config.ts so it populatesctx.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 cachingAuth 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 codeHard‑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 queriesIn 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 SetMinor 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 unauthorizedKeeps 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 unsoundConnectorType
assignment
connectorType
is declared asConnectorType
but populated from untyped JSON, which can beundefined
. 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 checksMove JSON parsing and validation into a schema to avoid
string
→ConnectorType
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 componentWrap 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 nitThe added
(id: string)
annotations are good. To avoid aliasing the original array, clonecurrentFavorites
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
permitsgoogle.com
underimages.remotePatterns
or useunoptimized
as shown.convex/import_export.ts (1)
47-52
: ReuseensureAuthenticated
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 existingusers
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 inensureAuthenticated
and a nullable variant ingetCurrentUserOrNull
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 removeas
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
alongsideauthenticatedDaily
.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 returnStrengthens 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 typeYou use
authUser.userId
(typed) in some calls anduserId
(Id<"users">) in others. Keep a singleconst 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 updateThis 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
⛔ 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 useor 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)
// 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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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.
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 | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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.
// 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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
There was a problem hiding this 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 requestIf 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 behaviorPer 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 conventionGuidelines 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 fortoken
to satisfy strict TS guidelines.Prevents accidental
any
ifgetToken
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 stylesA 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 pinKeeps 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 chatsLarge 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
📒 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 useor 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 appropriatelyUsing actions for checkout/portal generation fits the flow here.
20-20
: LGTM: centralized Better Auth client importImporting from lib/auth-client keeps auth calls consistent.
29-29
: LGTM: minimal user context usage
hasPremium
andproducts
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 goodExplicit
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 importlib/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 validatorsBetter 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 accessSafe 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 breakingAfter 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 behaviorThe mutations should either:
- Preserve null returns to maintain backward compatibility with existing error handling patterns
- Update return types to remove
v.null()
if throwing is the intended behaviorCurrent 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 queriesGood perf/accuracy improvement over filtering in memory.
Also applies to: 601-603
convex/schema.ts (1)
16-18
: LGTM: indices added for new access patternsby_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 usageAuth 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_originalChatIdSignature 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 idempotentIf 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 varsEmpty 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))
if (!token) { | ||
return createErrorResponse(new Error("Unauthorized")); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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.
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" }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
users: defineTable(User).index("email", ["email"]), | ||
chats: defineTable(Chat).index("by_user", ["userId"]), | ||
chats: defineTable(Chat) |
There was a problem hiding this comment.
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.
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) | |
}); |
Summary
Complete migration from
@convex-dev/auth
to Better Auth for improved flexibility, cleaner APIs, and better TypeScript support.Key Changes
• Authentication Infrastructure:
• Frontend Updates:
• Backend Integration:
• Data Preservation:
• Cleanup:
Migration Commits
Test Plan
Breaking Changes
None - this is a drop-in replacement maintaining existing functionality.
Technical Notes
safeGetAuthUser(ctx)
for safe authentication checksSummary 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
Dependencies
Summary by CodeRabbit
New Features
Refactor
Removals/Breaking Changes
Documentation
Chores