From 8f80353c94e3cca4d8adc2347a9f63d02481b72a Mon Sep 17 00:00:00 2001 From: Beau Mersereau Date: Wed, 13 May 2026 07:17:59 -0400 Subject: [PATCH 1/3] fix: add limit and before-cursor pagination to GET /chat Adds `limit` (default 50, max 200) and `before` (ISO timestamp cursor) query parameters to prevent unbounded chat list queries for active users. --- .../src/lib/__tests__/chatPagination.test.ts | 18 +++++++++++++++++ backend/src/routes/chat.ts | 20 +++++++++++++++++-- backend/tsconfig.json | 2 +- 3 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 backend/src/lib/__tests__/chatPagination.test.ts diff --git a/backend/src/lib/__tests__/chatPagination.test.ts b/backend/src/lib/__tests__/chatPagination.test.ts new file mode 100644 index 000000000..bdfef9191 --- /dev/null +++ b/backend/src/lib/__tests__/chatPagination.test.ts @@ -0,0 +1,18 @@ +import { readFileSync } from "fs"; +import { join } from "path"; +import { describe, it, expect } from "vitest"; + +describe("GET /chat pagination", () => { + const src = readFileSync( + join(__dirname, "../../routes/chat.ts"), + "utf8", + ); + + it("applies a limit to the chat list query", () => { + expect(src).toMatch(/\.limit\(/); + }); + + it("supports a before-cursor for pagination", () => { + expect(src).toMatch(/before|lt\("created_at"/); + }); +}); diff --git a/backend/src/routes/chat.ts b/backend/src/routes/chat.ts index fe272c671..821d56a2c 100644 --- a/backend/src/routes/chat.ts +++ b/backend/src/routes/chat.ts @@ -142,6 +142,15 @@ chatRouter.get("/", requireAuth, async (req, res) => { const userId = res.locals.userId as string; const db = createServerSupabase(); + // Parse pagination params + const rawLimit = Number(req.query.limit); + const limit = Number.isFinite(rawLimit) && rawLimit > 0 + ? Math.min(rawLimit, 200) + : 50; + const before = typeof req.query.before === "string" && req.query.before + ? req.query.before + : null; + const { data: ownProjects, error: projErr } = await db .from("projects") .select("id") @@ -156,11 +165,18 @@ chatRouter.get("/", requireAuth, async (req, res) => { ? `user_id.eq.${userId},project_id.in.(${ownProjectIds.join(",")})` : `user_id.eq.${userId}`; - const { data, error } = await db + let query = db .from("chats") .select("*") .or(filter) - .order("created_at", { ascending: false }); + .order("created_at", { ascending: false }) + .limit(limit); + + if (before) { + query = query.lt("created_at", before); + } + + const { data, error } = await query; if (error) return void res.status(500).json({ detail: error.message }); res.json(data ?? []); }); diff --git a/backend/tsconfig.json b/backend/tsconfig.json index a4b3abf67..80885d66d 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -16,5 +16,5 @@ } }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist", "src/**/__tests__"] } From 68122ce65c2db337dc785f541b4dd57a29b216fc Mon Sep 17 00:00:00 2001 From: Beau Mersereau Date: Wed, 13 May 2026 07:26:54 -0400 Subject: [PATCH 2/3] fix: add test script to backend package.json --- backend/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/package.json b/backend/package.json index 8451ab8b7..6f55da404 100644 --- a/backend/package.json +++ b/backend/package.json @@ -5,7 +5,8 @@ "scripts": { "dev": "tsx watch src/index.ts", "build": "tsc", - "start": "node dist/index.js" + "start": "node dist/index.js", + "test": "vitest run" }, "dependencies": { "@anthropic-ai/sdk": "^0.90.0", From 555f20bc05e8bcf257e5327191b87f8fffb6d2ff Mon Sep 17 00:00:00 2001 From: Beau Mersereau Date: Wed, 13 May 2026 07:28:09 -0400 Subject: [PATCH 3/3] fix: validate before cursor is a valid ISO timestamp, return 400 on bad input --- backend/src/routes/chat.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/backend/src/routes/chat.ts b/backend/src/routes/chat.ts index 821d56a2c..34c850d58 100644 --- a/backend/src/routes/chat.ts +++ b/backend/src/routes/chat.ts @@ -147,9 +147,11 @@ chatRouter.get("/", requireAuth, async (req, res) => { const limit = Number.isFinite(rawLimit) && rawLimit > 0 ? Math.min(rawLimit, 200) : 50; - const before = typeof req.query.before === "string" && req.query.before - ? req.query.before - : null; + const rawBefore = typeof req.query.before === "string" ? req.query.before : null; + if (rawBefore !== null && Number.isNaN(new Date(rawBefore).getTime())) { + return void res.status(400).json({ detail: "before must be a valid ISO 8601 timestamp" }); + } + const before = rawBefore; const { data: ownProjects, error: projErr } = await db .from("projects")