Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 49 additions & 15 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
@@ -1,29 +1,63 @@
language: en

reviews:
profile: chill
request_changes_workflow: false
profile: assertive
request_changes_workflow: true
high_level_summary: true
poem: false
review_status: true
auto_review:
enabled: true
drafts: false
path_filters:
- "!client/src/generated/**"
- "!client/node_modules/**"
- "!client/.next/**"
- "client/src/**"
- "extension/**"

chat:
auto_reply: true

instructions: |
Recall is an AI-powered bookmark manager built with Next.js 16 (App Router),
TypeScript, Prisma v7, Supabase (PostgreSQL), Tailwind CSS, and Google Gemini API.
The Prisma client uses @prisma/adapter-pg.

The developer is experienced with the MERN stack but is transitioning to this stack.

When reviewing code:
- Explain issues clearly — state WHY something is wrong, not just that it is.
- If a concept is being used incorrectly, give the MERN/Express equivalent to bridge understanding.
- Flag incorrect Server vs Client component usage (Next.js App Router logic).
- Suggest more efficient Prisma queries (e.g., avoiding N+1 issues).
- Point out security issues (exposed secrets, SQL injection, XSS) explicitly.
- Focus on correctness and security — avoid nitpicking stylistic preferences.
Recall is an AI-powered bookmark manager. Stack: Next.js 16 (App Router),
TypeScript, Prisma v7 with @prisma/adapter-pg, Supabase (PostgreSQL + Auth),
Tailwind CSS v4, Google Gemini API, Upstash Redis for rate limiting.
Live at recallsave.vercel.app. This is a production app.

The developer is experienced with MERN but is learning this stack.
Explain WHY something is wrong and give the Express/Mongoose equivalent
when relevant.

Security rules — flag any violation as a blocking issue:
- Every API route must call getUserFromRequest(req) or supabase.auth.getUser()
before touching Prisma or any user data. No exceptions.
- Never trust userId from req.body — it must always come from the verified session.
- Any server-side URL fetch (scraper, thumbnails) must go through isSafeUrl()
from lib/url-validation.ts to prevent SSRF.
- Every POST and DELETE route must have rate limiting via bookmarkRatelimit,
tagRatelimit, collectionRatelimit, or a named limiter from lib/ratelimit.ts.
- No secret should use NEXT_PUBLIC_ prefix.
NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY are intentionally
public — do not flag these.
- dangerouslySetInnerHTML is banned everywhere except the theme script in layout.tsx.

Prisma rules:
- Every findMany must have a take: limit. Flag any findMany without one.
- Avoid N+1: use include:{} or select:{} to fetch relations in one query.
- Never pass raw user input into a Prisma where clause without type-checking it first.

Accessibility rules:
- Every icon-only button must have an aria-label.
- Every input must have a label or aria-label.
- Buttons hidden with opacity-0 must also have focus-visible:opacity-100.

Next.js App Router rules:
- Flag incorrect Server vs Client component usage.
- Server Components must never import client-only hooks (useState, useEffect).
- "use client" components must never call Prisma directly.

Performance rules:
- No new Google Fonts without removing one.
- Every Next.js Image must have a sizes prop.
- Flag unoptimized={true} on any Image component.
11 changes: 10 additions & 1 deletion client/src/app/api/bookmarks/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { getUserFromRequest } from "@/lib/supabase/get-user";
import { bookmarkRatelimit } from "@/lib/ratelimit";

export async function DELETE(
req: NextRequest,
Expand All @@ -12,6 +13,13 @@ export async function DELETE(
if (!user)
return NextResponse.json({ error: "unauthorized" }, { status: 401 });

const { success } = await bookmarkRatelimit.limit(user.id);
if (!success)
return NextResponse.json(
{ error: "too many requests - slow down" },
{ status: 429 },
);

try {
const bookmark = await prisma.bookmark.findFirst({
where: { id, userId: user.id },
Expand All @@ -25,7 +33,8 @@ export async function DELETE(
}

await prisma.bookmark.delete({ where: { id: bookmark.id } });
} catch {
} catch (err) {
console.error("Failed to delete bookmark:", err);
return NextResponse.json(
{ error: "failed to delete bookmark" },
{ status: 500 },
Expand Down
3 changes: 3 additions & 0 deletions client/src/app/api/bookmarks/[id]/tags/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ export async function POST(
if (!name) {
return NextResponse.json({ error: "name is required" }, { status: 400 });
}
if (name.length > 50) {
return NextResponse.json({ error: "tag name too long" }, { status: 400 });
}

let bookmark;
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { prisma } from "@/lib/prisma";
import { getUserFromRequest } from "@/lib/supabase/get-user";
import { collectionRatelimit } from "@/lib/ratelimit";
import { NextRequest } from "next/server";

// DELETE /api/collections/[id]/bookmarks/[bookmarkId] — remove a bookmark from a collection
Expand All @@ -10,6 +11,10 @@ export async function DELETE(
const user = await getUserFromRequest(req);
if (!user) return Response.json({ error: "unauthorized" }, { status: 401 });

const { success } = await collectionRatelimit.limit(user.id);
if (!success)
return Response.json({ error: "too many requests - slow down" }, { status: 429 });

const { id: collectionId, bookmarkId } = await params;

// Verify collection belongs to user
Expand Down
5 changes: 5 additions & 0 deletions client/src/app/api/collections/[id]/bookmarks/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { prisma } from "@/lib/prisma";
import { getUserFromRequest } from "@/lib/supabase/get-user";
import { collectionRatelimit } from "@/lib/ratelimit";
import { NextRequest } from "next/server";

// POST /api/collections/[id]/bookmarks — add a bookmark to a collection
Expand All @@ -10,6 +11,10 @@ export async function POST(
const user = await getUserFromRequest(req);
if (!user) return Response.json({ error: "unauthorized" }, { status: 401 });

const { success } = await collectionRatelimit.limit(user.id);
if (!success)
return Response.json({ error: "too many requests - slow down" }, { status: 429 });

const { id: collectionId } = await params;

// Verify collection belongs to user
Expand Down
5 changes: 5 additions & 0 deletions client/src/app/api/collections/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { prisma } from "@/lib/prisma";
import { getUserFromRequest } from "@/lib/supabase/get-user";
import { collectionRatelimit } from "@/lib/ratelimit";
import { NextRequest } from "next/server";

// DELETE /api/collections/[id]
Expand All @@ -10,6 +11,10 @@ export async function DELETE(
const user = await getUserFromRequest(req);
if (!user) return Response.json({ error: "unauthorized" }, { status: 401 });

const { success } = await collectionRatelimit.limit(user.id);
if (!success)
return Response.json({ error: "too many requests - slow down" }, { status: 429 });

const { id } = await params;

const collection = await prisma.collection.findUnique({ where: { id } });
Expand Down
5 changes: 5 additions & 0 deletions client/src/app/api/collections/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { prisma } from "@/lib/prisma";
import { getUserFromRequest } from "@/lib/supabase/get-user";
import { collectionRatelimit } from "@/lib/ratelimit";
import { NextRequest, NextResponse } from "next/server";

// GET /api/collections — list all collections for the user
Expand All @@ -20,6 +21,10 @@ export async function POST(req: NextRequest) {
const user = await getUserFromRequest(req);
if (!user) return NextResponse.json({ error: "unauthorized" }, { status: 401 });

const { success } = await collectionRatelimit.limit(user.id);
if (!success)
return NextResponse.json({ error: "too many requests - slow down" }, { status: 429 });

let body: { name?: unknown };
try {
body = await req.json();
Expand Down
7 changes: 7 additions & 0 deletions client/src/lib/ratelimit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,10 @@ export const tagRatelimit = new Ratelimit({
limiter: Ratelimit.slidingWindow(30, "1 h"),
prefix: "recall:tag",
});

// 60 collection operations per user per hour
export const collectionRatelimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(60, "1 h"),
prefix: "recall:collection",
});
Loading