Skip to content

Implement Clerk↔Supabase Auth, Chat CRUD, Document RAG, and Real-Time Collaboration#730

Merged
ngoiyaeric merged 3 commits into
mainfrom
feat/clerk-supabase-rag-realtime-13454999576171299600
Jul 11, 2026
Merged

Implement Clerk↔Supabase Auth, Chat CRUD, Document RAG, and Real-Time Collaboration#730
ngoiyaeric merged 3 commits into
mainfrom
feat/clerk-supabase-rag-realtime-13454999576171299600

Conversation

@ngoiyaeric

@ngoiyaeric ngoiyaeric commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

This pull request implements the four core tasks:

  1. Clerk ↔ Supabase end-user auth bridge with real, identity-aware clients on the browser and server.
  2. Completed Chat CRUD surface supporting rename, visibility updates, and single session deletion.
  3. Document Attachment and RAG Pipeline parsing, embedding (OpenAI), and retrieving matching vector chunks.
  4. Real-time multi-user chat collaboration via Supabase Realtime subscriptions.

PR created automatically by Jules for task 13454999576171299600 started by @ngoiyaeric

Summary by CodeRabbit

  • New Features
    • Upload PDFs, text/Markdown, and common document formats in chats, and ingest them into searchable content.
    • Ask questions about uploaded documents to retrieve relevant passages in the UI.
    • Share chats via a new “Share & Collaborate” workflow: invite, remove, and manage participants.
    • Chat and participant changes now refresh in real time.
    • Chat owners can update title/visibility and delete chats.
  • Documentation
    • Added Clerk–Supabase native token validation setup guidance and clarified dual-write consistency/access-control guarantees.

Implements Task 1 Clerk-Supabase auth bridge, Task 2 history CRUD,
Task 3 Document ingestion and retrieval RAG, and Task 4 multi-user realtime collaboration.

Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
@google-labs-jules

Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@qodo-code-review

Copy link
Copy Markdown
Contributor

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@CLAassistant

CLAassistant commented Jul 11, 2026

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
0 out of 2 committers have signed the CLA.

❌ ngoiyaeric
❌ google-labs-jules[bot]


ngoiyaeric seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds document upload, embedding, vector retrieval, and rendered results, while enabling participant-based chat collaboration, Clerk-authenticated Supabase access, realtime updates, and message consistency safeguards.

Changes

Document ingestion and retrieval

Layer / File(s) Summary
Document data model and access policies
drizzle/migrations/*, lib/db/schema.ts, lib/schema/document-retrieve.tsx, package.json
Adds document and chunk tables, relations, vector matching, storage policies, retrieval validation, and migration registration.
Attachment upload and ingestion
components/chat-panel.tsx, app/actions.tsx
Uploads supported non-image attachments and converts their text into embedded document chunks during submission.
Retrieval tool and rendering
lib/agents/*, app/actions.tsx
Routes document questions to documentRetrieve, searches matching chunks, streams results, and renders retrieved content.

Collaborative chat and Clerk-authenticated Supabase

Layer / File(s) Summary
Clerk-authenticated Supabase access
lib/supabase/*, middleware.ts, CLERK_SUPABASE_FIX.md
Replaces Supabase mocks with Clerk-token-aware clients, expands action-route middleware coverage, and documents native Clerk integration and consistency behavior.
Participant-aware chat access and persistence
lib/actions/chat-db.ts, lib/actions/chat.ts, lib/types/index.ts
Adds participant access, chat metadata actions, per-message ownership, and duplicate-safe message upserts.
Participant API and collaboration dialog
app/api/chats/[id]/participants/route.ts, components/chat-share.tsx
Adds authenticated participant endpoints and invite/list/remove controls.
Realtime chat refresh
components/chat.tsx
Refreshes chats when message or participant changes arrive through Supabase Realtime.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ChatPanel
  participant SupabaseStorage
  participant SubmitAction
  participant DocumentRetrieveTool
  participant SupabaseDatabase
  User->>ChatPanel: Select and submit document
  ChatPanel->>SupabaseStorage: Upload attachment
  ChatPanel->>SubmitAction: Send storage metadata
  SubmitAction->>SupabaseStorage: Download document
  SubmitAction->>SupabaseDatabase: Store document and embedded chunks
  User->>DocumentRetrieveTool: Ask about uploaded document
  DocumentRetrieveTool->>SupabaseDatabase: Match document chunks
  SupabaseDatabase-->>DocumentRetrieveTool: Return ranked chunks
  DocumentRetrieveTool-->>User: Render retrieved content
Loading

Possibly related issues

Possibly related PRs

  • QueueLab/QCX#303: Overlaps on uploaded-file handling in the chat submission pipeline.
  • QueueLab/QCX#505: Overlaps on message upsert behavior in lib/actions/chat-db.ts.
  • QueueLab/QCX#705: Overlaps at the Clerk authentication and Supabase user-ID boundary.

Suggested labels: Backend, Review effort 4/5, Possible security concern

Poem

A rabbit uploads a file with a hop,
Embeds its words in a vector crop.
Friends join the chat,
Realtime goes splat—
Clerk guards the burrow nonstop!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main additions: Clerk-Supabase auth, chat CRUD, document RAG, and realtime collaboration.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/clerk-supabase-rag-realtime-13454999576171299600

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


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

❤️ Share

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

@vercel

vercel Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
qcx Ready Ready Preview, Comment Jul 11, 2026 10:47am

@charliecreates charliecreates Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Found four blocking issues in the new document/RAG flow and related changes.

})

// 2. Query DB via the match_document_chunks RPC
const rpcResult = await db.execute(sql`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

db is the privileged Postgres connection (lib/db/index.ts), so this query does not receive Supabase RLS protection. documentId is optional and no authenticated user/chat constraint is supplied; the RPC also scans every document_chunks row when it is null. As a result, any user whose agent invokes this tool can receive chunks from other users' documents. Resolve the current user server-side and join/filter through documents to that user's documents (or an explicitly authorized chat) before returning content; do not rely on an LLM-provided optional document ID for isolation.

Comment thread app/actions.tsx Outdated
import { nanoid } from '@/lib/utils'
import type { FeatureCollection } from 'geojson'
import { Spinner } from '@/components/ui/spinner'
import { Spinner } from '@/components/spinner'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This module does not exist at the PR head—the repository only has components/ui/spinner.tsx. The changed import makes app/actions.tsx fail module resolution during the production build. Keep the prior @/components/ui/spinner import (or add the referenced module).

Comment thread app/actions.tsx Outdated
throw downloadError
}

const text = fileData ? await fileData.text() : ''

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The picker advertises application/pdf, but Blob.text() does not extract PDF content; it decodes the binary PDF bytes as text and those bytes are then embedded. PDF uploads will therefore complete with unusable retrieval data. Parse PDFs with a PDF text extractor (and reject/handle unsupported MIME types) before chunking and embedding.

Comment thread lib/db/schema.ts Outdated
chat: one(chats, {
fields: [locations.chatId],
references: [chats.id],
references: [locations.id],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This relation is from locations.chatId to the chats table, so its reference must remain chats.id. Pointing it at locations.id produces an invalid self-join condition for Drizzle relational queries and breaks loading a location's associated chat.

…ports, fix schema relation, add userId to AIMessage

- Add 'postgres' package as explicit dependency (required by lib/db/index.ts which imports it directly; drizzle-orm lists it as a peer dep but it was never installed)
- Fix Spinner import path in app/actions.tsx and components/chat-panel.tsx (component lives at components/ui/spinner.tsx, not components/spinner.tsx)
- Fix Spinner usage in chat-panel.tsx (component doesn't accept className prop)
- Fix locationsRelations.chat references from [locations.id] to [chats.id]
- Add optional 'userId' field to AIMessage type to satisfy lib/actions/chat.ts usage

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 17

Caution

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

⚠️ Outside diff range comments (1)
lib/actions/chat-db.ts (1)

123-147: 🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift

Do not randomize duplicate IDs and blindly upsert by the global message ID.

Random replacement IDs are unstable across saves, so the same duplicated AI-state batch creates additional rows each time. More critically, ON CONFLICT (id) DO UPDATE has no chat or author guard: supplying an existing message ID can overwrite its content and role even when it belongs to another chat.

Fix the upstream producers to assign unique stable IDs, reject duplicates in a batch, and treat messages as append-only unless an explicit authorized update path is required.

Safer immediate behavior
-        while (seenIds.has(id)) {
-          id = crypto.randomUUID();
-        }
+        if (seenIds.has(id)) {
+          throw new Error(`Duplicate message ID in saveChat batch: ${id}`);
+        }
         seenIds.add(id);
...
-      await tx.insert(messages).values(messagesToInsert).onConflictDoUpdate({ target: messages.id, set: { content: sql`EXCLUDED.content`, role: sql`EXCLUDED.role` } });
+      await tx.insert(messages).values(messagesToInsert)
+        .onConflictDoNothing({ target: messages.id });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/actions/chat-db.ts` around lines 123 - 147, Remove the random UUID
regeneration and global-ID upsert behavior in the message persistence flow
around messagesToInsert and tx.insert(messages). Update the upstream message
producers to assign unique stable IDs, reject duplicate IDs within a batch, and
insert messages append-only without overwriting existing records; preserve
updates only through an explicit authorized path that validates chat and author
ownership.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/actions.tsx`:
- Around line 349-375: In the server-side ingestion flow guarded by
documentStoragePath and userId, validate the downloaded object’s actual size and
MIME type instead of trusting documentMime, then reject unsupported or oversized
files before chunking. Cap the extracted chunks before calling embedMany,
preserving the existing processing flow only for uploads within all configured
limits.
- Around line 390-399: Update the document ingestion flow around the initial
documents insert and its catch block to retain the created row ID, then update
that existing row to status "error" on processing failures instead of inserting
a duplicate. Only insert a new error row when the initial creation did not
produce an ID.

In `@app/api/chats/`[id]/participants/route.ts:
- Around line 15-17: Update the participants route around listParticipants and
its underlying action to distinguish “no collaborators” from denied access
instead of treating both as an empty array. Return an explicit null or
authorization result for inaccessible chats, then map that result to an
appropriate 403 or 404 response while preserving the 200 response for authorized
chats with no participants.
- Around line 34-41: Validate the untrusted role input in the participant route
before calling addParticipant: for the current single-role model, ignore the
request’s role value and always pass the supported collaborator role. Keep the
existing emailOrClerkId validation and persistence flow unchanged.

In `@CLERK_SUPABASE_FIX.md`:
- Around line 164-171: Revise the “Dual-Write Consistency and Access Control
Guarantees” section to match the actual saveChat persistence behavior, removing
claims about per-user ownership stamping, deterministic ID-based deduplication,
and unrestricted conflict updates unless those guarantees are implemented. First
fix saveChat to validate message ownership, preserve stable IDs, and restrict
conflict updates to authorized records, then document only the guarantees it
truly provides.
- Around line 139-160: The native Clerk/Supabase documentation mixes direct JWKS
authentication with the legacy `getToken({ template: 'supabase' })` and
`auth.uid()`/`clerk_user_id` bridge. Update the “Application Integration”
section to describe only one model: either document native client token usage
and corresponding native RLS policies, or relabel the setup as legacy and retain
the bridge-specific clients and policies consistently.

In `@components/chat-panel.tsx`:
- Around line 145-149: The upload failure handling in the chat submission flow
must prevent the optimistic attachment message from remaining after an
unsuccessful upload. Update the surrounding message-append logic near the upload
catch block to append only after upload success, or remove the optimistic entry
in the catch path, while preserving the existing error alert and upload-state
reset.

In `@components/chat-share.tsx`:
- Around line 125-133: Add an accessible label for the invitation input in the
collaborator form handled by handleAddCollaborator, using a visible label or an
appropriate aria-label. Ensure it clearly identifies the field as accepting an
email address or Clerk user ID while preserving the existing input behavior.
- Around line 29-48: Clear the existing participants at the start of
fetchParticipants, before requesting the new chat’s data, so changing chatId
cannot display the previous chat’s names or emails. Also clear participants when
the fetch response is unsuccessful or the request throws, while preserving the
existing loading-state cleanup in finally.

In `@components/chat.tsx`:
- Around line 103-127: Update the realtime handling around the messages and
chat_participants subscriptions to support removal events without relying on
filtered DELETE payloads, which lack replica identity. Add a soft-delete field
or implement another reliable removal-event path, then ensure the affected
subscription callbacks refresh the UI when records are removed.
- Around line 111-126: Update the realtime handlers in the chat component so
message changes refresh the useUIState() messages from the latest server data
instead of relying only on router.refresh(). Reinitialize or reset that UI state
whenever the chat id changes, while preserving the existing collaborator refresh
behavior and using the component’s established server-data source.

In `@drizzle/migrations/0005_add_documents_rag.sql`:
- Around line 63-70: Update the “Allow users to read their own attachments”
SELECT policy on storage.objects to use the non-deprecated owner_id column and
require it to match auth.uid() for every read. Remove the public.is_clerk_user
branch so Clerk authentication cannot bypass object ownership, while preserving
the chat-attachments bucket restriction.

In `@lib/agents/tools/document-retrieve.tsx`:
- Around line 28-35: Update the document retrieval flow around the privileged
db.execute call to resolve the authenticated user server-side and constrain
match_document_chunks results through documents owned by that user or an
explicitly authorized chat. Treat the LLM-provided documentId only as an
additional filter, never as the access-control boundary, and ensure null
documentId cannot search across other users’ documents.
- Line 15: Update the tool implementation around execute to infer its input
parameter type from the Zod schema instead of an inline cast, and replace
any[]/rpcResult as any[]/r: any with an explicit type matching the RPC response.
Rename or remove hasError so the empty-results case is represented as a valid
no-results state, while preserving the existing early return for caught
exceptions and the current response behavior.

In `@lib/db/schema.ts`:
- Line 129: Update the embedding column declaration in the schema to pass the
required dimensions configuration to vector, setting dimensions to 1536 so it
matches the database column definition.

In `@lib/supabase/browser-client.ts`:
- Around line 7-32: Update the browser client’s Supabase configuration to
provide the Clerk JWT through the `auth.accessToken` callback, reusing the
existing Clerk session token retrieval and warning behavior. Remove the custom
`global.fetch` Authorization-header injection, while preserving `persistSession:
false` and returning null when no browser session token is available.

In `@lib/supabase/client.ts`:
- Around line 8-32: Update the Supabase client initialization in the surrounding
client factory to use Clerk’s native accessToken callback, returning the token
from auth().getToken() without the legacy supabase template. Remove the custom
global.fetch implementation, including manual token retrieval, Authorization
header injection, and related warning handling, while preserving the existing
auth configuration.

---

Outside diff comments:
In `@lib/actions/chat-db.ts`:
- Around line 123-147: Remove the random UUID regeneration and global-ID upsert
behavior in the message persistence flow around messagesToInsert and
tx.insert(messages). Update the upstream message producers to assign unique
stable IDs, reject duplicate IDs within a batch, and insert messages append-only
without overwriting existing records; preserve updates only through an explicit
authorized path that validates chat and author ownership.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b2a9330a-18d5-473c-a948-0e71cea138fd

📥 Commits

Reviewing files that changed from the base of the PR and between 6388bde and 08a2a55.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (19)
  • CLERK_SUPABASE_FIX.md
  • app/actions.tsx
  • app/api/chats/[id]/participants/route.ts
  • components/chat-panel.tsx
  • components/chat-share.tsx
  • components/chat.tsx
  • drizzle/migrations/0005_add_documents_rag.sql
  • drizzle/migrations/meta/_journal.json
  • lib/actions/chat-db.ts
  • lib/actions/chat.ts
  • lib/agents/researcher.tsx
  • lib/agents/tools/document-retrieve.tsx
  • lib/agents/tools/index.tsx
  • lib/db/schema.ts
  • lib/schema/document-retrieve.tsx
  • lib/supabase/browser-client.ts
  • lib/supabase/client.ts
  • lib/types/index.ts
  • package.json
📜 Review details
🧰 Additional context used
🪛 LanguageTool
CLERK_SUPABASE_FIX.md

[style] ~171-~171: ‘exact same’ might be wordy. Consider a shorter alternative.
Context: ...abase Realtime subscriptions follow the exact same RLS policies deployed in QCX-BACKEND. B...

(EN_WORDINESS_PREMIUM_EXACT_SAME)

🪛 markdownlint-cli2 (0.22.1)
CLERK_SUPABASE_FIX.md

[warning] 143-143: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 148-148: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 156-156: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

🪛 React Doctor (0.5.8)
components/chat-share.tsx

[warning] 126-126: Blind users can't tell what this control does because screen readers find no label, so add visible text, aria-label, or aria-labelledby.

Give every interactive control a label screen readers can read.

(control-has-associated-label)


[warning] 161-161: Your users can submit the form by accident because a <button> with no type defaults to submit.

Set an explicit button type so plain buttons do not submit forms by accident: type="button", "submit", or "reset".

(button-has-type)

🪛 SQLFluff (4.2.2)
drizzle/migrations/0005_add_documents_rag.sql

[error] 23-23: ADD CONSTRAINT ... FOREIGN KEY should use NOT VALID to avoid locking the table while validating existing rows.

(PG01)


[error] 24-24: ADD CONSTRAINT ... FOREIGN KEY should use NOT VALID to avoid locking the table while validating existing rows.

(PG01)


[error] 25-25: ADD CONSTRAINT ... FOREIGN KEY should use NOT VALID to avoid locking the table while validating existing rows.

(PG01)


[error] 32-32: Do not use special characters in identifiers.

(RF05)


[error] 33-33: Do not use special characters in identifiers.

(RF05)


[error] 39-39: Do not use special characters in identifiers.

(RF05)


[error] 40-40: Do not use special characters in identifiers.

(RF05)


[error] 53-53: Do not use special characters in identifiers.

(RF05)


[error] 54-54: Do not use special characters in identifiers.

(RF05)


[error] 59-59: The 'WHERE' keyword should always start a new line.

(LT14)


[error] 63-63: Do not use special characters in identifiers.

(RF05)


[error] 64-64: Do not use special characters in identifiers.

(RF05)


[error] 69-69: The 'WHERE' keyword should always start a new line.

(LT14)

🔇 Additional comments (19)
components/chat.tsx (2)

25-25: LGTM!

Also applies to: 274-274


98-102: 🎯 Functional Correctness

Drop this comment — Realtime already receives the client's auth state here. The Supabase v2 client propagates the configured auth flow to Realtime, so this channel is not joining anonymously; setAuth is only needed for custom JWT setups.

			> Likely an incorrect or invalid review comment.
lib/actions/chat-db.ts (1)

2-2: LGTM!

Also applies to: 16-80, 198-230

lib/actions/chat.ts (2)

293-396: LGTM!


213-219: 🔒 Security & Privacy

No issue: msg.userId is already server-stamped or absent The current call paths either set message ownership on the server or leave it empty, so this fallback does not create a new trust boundary.

			> Likely an incorrect or invalid review comment.
lib/types/index.ts (1)

59-79: LGTM!

app/api/chats/[id]/participants/route.ts (1)

53-80: LGTM!

components/chat-share.tsx (1)

50-98: LGTM!

Also applies to: 117-124, 134-178

drizzle/migrations/0005_add_documents_rag.sql (2)

1-62: LGTM!


74-99: 🔒 Security & Privacy

Keep the global fallback or make documentId required
match_document_chunks() runs under the caller’s RLS, and document_chunks already has a SELECT policy that scopes rows to the owning document/user. The nullable fallback only broadens within what the caller can already read.

			> Likely an incorrect or invalid review comment.
drizzle/migrations/meta/_journal.json (1)

39-45: LGTM!

lib/db/schema.ts (1)

114-128: LGTM!

Also applies to: 130-131, 143-143, 156-156, 243-261

package.json (1)

86-86: LGTM!

components/chat-panel.tsx (1)

8-8: LGTM!

Also applies to: 18-18, 38-38, 106-111, 254-254, 265-265, 277-280, 324-328, 338-338

app/actions.tsx (1)

9-51: LGTM!

Also applies to: 803-818

lib/schema/document-retrieve.tsx (1)

1-9: LGTM!

lib/agents/researcher.tsx (1)

60-64: LGTM!

Also applies to: 80-83

lib/agents/tools/index.tsx (1)

5-6: LGTM!

Also applies to: 25-28, 47-47

lib/agents/tools/document-retrieve.tsx (1)

22-25: 🚀 Performance & Scalability

No vector-dimension mismatch here The migration already uses vector(1536) for both document_chunks.embedding and match_document_chunks.query_embedding, so this text-embedding-ada-002 call fits the current schema.

			> Likely an incorrect or invalid review comment.

Comment thread app/actions.tsx
Comment thread app/actions.tsx Outdated
Comment thread app/api/chats/[id]/participants/route.ts Outdated
Comment thread app/api/chats/[id]/participants/route.ts Outdated
Comment thread CLERK_SUPABASE_FIX.md Outdated
Comment on lines +63 to +70
DROP POLICY IF EXISTS "Allow users to read their own attachments" ON storage.objects;
CREATE POLICY "Allow users to read their own attachments" ON storage.objects
FOR SELECT TO authenticated USING (
bucket_id = 'chat-attachments'
AND (
auth.uid() = owner
OR public.is_clerk_user((SELECT id FROM public.users WHERE public.users.id = auth.uid()))
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify no other permissive SELECT policy exposes this bucket.
rg -n -C5 'chat-attachments|owner_id|\bowner\b|is_clerk_user' drizzle

Repository: QueueLab/QCX

Length of output: 30280


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== storage ownership references ==\n'
rg -n -C3 '\bowner_id\b|\bowner\b|storage\.objects|chat-attachments|is_clerk_user' drizzle supabase .github README.md || true

printf '\n== migration file around storage policy ==\n'
sed -n '49,72p' drizzle/migrations/0005_add_documents_rag.sql

printf '\n== is_clerk_user definition ==\n'
sed -n '132,150p' drizzle/migrations/0003_clerk_auth_sync.sql

printf '\n== schema hints for storage.objects columns ==\n'
rg -n -C2 'create table.*storage\.objects|table storage\.objects|owner_id|metadata' drizzle supabase .github README.md || true

Repository: QueueLab/QCX

Length of output: 27697


🌐 Web query:

Supabase storage.objects owner owner_id documentation ownership select policy auth.uid clerk user

💡 Result:

In Supabase Storage, the owner of an object is stored in the owner_id column of the storage.objects table [1][2]. The value in this column is derived from the sub (subject) claim within the user's JWT [1][2]. Key points regarding ownership and policies: 1. Recommended Column: Use the owner_id column for policy definitions [2]. While an older owner column exists, it is deprecated [2]. 2. Defining Policies: To restrict access to objects so that only the owner can interact with them (e.g., SELECT or DELETE), you can compare the owner_id against the authenticated user's ID using auth.uid [2][3]. Example policy for deletion: create policy "User can delete their own objects" on storage.objects for delete to authenticated using ( owner_id = (select auth.uid::text)); Integration with Clerk: Integrating Supabase Storage with Clerk requires caution regarding user ID formats [4][5]. Clerk typically uses non-UUID string IDs, whereas Supabase Storage historically expected UUIDs for its owner-related fields [4][5]. - If you experience errors when using Clerk, it is often because the Clerk sub (user ID) does not conform to the UUID format expected by the database column [4][5]. - Workarounds often involve casting the owner_id to text or ensuring your JWT claims match the expected type [6]. - Always verify your specific setup against the official Supabase and Clerk integration guides, which recommend creating RLS policies that correctly interpret the Clerk user ID from the authentication token [7][8]. Note: When resources are created using the service_role key or via the Supabase Dashboard, the owner_id may not be set [2]. In these cases, the resource is effectively owned by anyone (or considered unowned) [2].

Citations:


Scope attachment reads to the current object owner drizzle/migrations/0005_add_documents_rag.sql:63-70 — the Clerk branch bypasses object ownership, so any authenticated Clerk user can read every object in chat-attachments. Use the storage ownership column (owner_id; owner is deprecated) to tie SELECT to the object itself.

🧰 Tools
🪛 SQLFluff (4.2.2)

[error] 63-63: Do not use special characters in identifiers.

(RF05)


[error] 64-64: Do not use special characters in identifiers.

(RF05)


[error] 69-69: The 'WHERE' keyword should always start a new line.

(LT14)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@drizzle/migrations/0005_add_documents_rag.sql` around lines 63 - 70, Update
the “Allow users to read their own attachments” SELECT policy on storage.objects
to use the non-deprecated owner_id column and require it to match auth.uid() for
every read. Remove the public.is_clerk_user branch so Clerk authentication
cannot bypass object ownership, while preserving the chat-attachments bucket
restriction.

Comment thread lib/agents/tools/document-retrieve.tsx
Comment thread lib/db/schema.ts
id: uuid('id').primaryKey().defaultRandom(),
documentId: uuid('document_id').notNull().references(() => documents.id, { onDelete: 'cascade' }),
chunkText: text('chunk_text').notNull(),
embedding: vector('embedding'),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT

curl -fsSL https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.2.tgz |
  tar -xz -C "$tmp"

fd 'vector\.d\.ts' "$tmp/package" --exec sed -n '1,160p' {}

Repository: QueueLab/QCX

Length of output: 7936


Declare the embedding dimensions here. vector('embedding') is missing the required dimensions config, so it won’t match the vector(1536) column definition. Use vector('embedding', { dimensions: 1536 }).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/db/schema.ts` at line 129, Update the embedding column declaration in the
schema to pass the required dimensions configuration to vector, setting
dimensions to 1536 so it matches the database column definition.

Comment on lines +7 to +32
return createSupabaseClient(supabaseUrl, supabaseAnonKey, {
global: {
async fetch(url, options = {}) {
let clerkToken = null;
if (typeof window !== 'undefined') {
try {
clerkToken = await (window as any).Clerk?.session?.getToken({ template: 'supabase' });
} catch (e) {
console.warn('[Supabase Browser Client] Failed to get Clerk token:', e);
}
}

const headers = new Headers(options.headers);
if (clerkToken) {
headers.set('Authorization', `Bearer ${clerkToken}`);
}

return fetch(url, {
...options,
headers,
});
},
},
auth: {
persistSession: false,
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -a -t f 'package\.json|pnpm-lock\.yaml|package-lock\.json|yarn\.lock|bun\.lock' . \
  | xargs rg -n -C2 '`@supabase/supabase-js`'

Repository: QueueLab/QCX

Length of output: 2408


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== browser client ==\n'
sed -n '1,200p' lib/supabase/browser-client.ts

printf '\n== chat usage ==\n'
sed -n '1,220p' components/chat.tsx

printf '\n== supabase helpers ==\n'
rg -n "createSupabaseClient|createClient\\(|accessToken|getToken\\(" lib components app src

Repository: QueueLab/QCX

Length of output: 9932


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== lib/supabase/client.ts ==\n'
sed -n '1,160p' lib/supabase/client.ts

printf '\n== all Clerk token calls ==\n'
rg -n "getToken\\(|template: 'supabase'|accessToken:" lib components app

Repository: QueueLab/QCX

Length of output: 1446


Route Supabase auth through accessToken
global.fetch only attaches the Clerk JWT to REST calls; Realtime subscriptions in components/chat.tsx still connect without a token and can fail RLS checks. Use Supabase’s accessToken callback in the browser client instead; the pinned @supabase/supabase-js version already supports it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/supabase/browser-client.ts` around lines 7 - 32, Update the browser
client’s Supabase configuration to provide the Clerk JWT through the
`auth.accessToken` callback, reusing the existing Clerk session token retrieval
and warning behavior. Remove the custom `global.fetch` Authorization-header
injection, while preserving `persistSession: false` and returning null when no
browser session token is available.

Comment thread lib/supabase/client.ts
Comment on lines +8 to +32
return createSupabaseClient(supabaseUrl, supabaseAnonKey, {
global: {
async fetch(url, options = {}) {
let clerkToken = null;
try {
const authObj = await auth();
clerkToken = await authObj.getToken({ template: 'supabase' });
} catch (e) {
console.warn('[Supabase Server Client] Failed to get Clerk token from server session:', e);
}

const headers = new Headers(options.headers);
if (clerkToken) {
headers.set('Authorization', `Bearer ${clerkToken}`);
}

return fetch(url, {
...options,
headers,
});
},
},
auth: {
persistSession: false,
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -a -t f 'package\.json|pnpm-lock\.yaml|package-lock\.json|yarn\.lock|bun\.lock' . \
  | xargs rg -n -C2 '`@supabase/supabase-js`'

Repository: QueueLab/QCX

Length of output: 2408


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## lib/supabase/client.ts\n'
cat -n lib/supabase/client.ts

printf '\n## Search Clerk/Supabase usage\n'
rg -n -C 3 "(getToken\\(|accessToken:|createSupabaseClient|`@clerk`|supabase)" lib app src . --glob '!bun.lock' --glob '!package.json' --glob '!package-lock.json' --glob '!yarn.lock' --glob '!pnpm-lock.yaml'

Repository: QueueLab/QCX

Length of output: 19915


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## package.json supabase-related deps\n'
cat -n package.json | sed -n '45,70p'

printf '\n## supabase SSR docs references in repo\n'
rg -n -C 2 "`@supabase/ssr`|getToken\\(|accessToken" . --glob '!bun.lock' --glob '!package.json' --glob '!package-lock.json' --glob '!yarn.lock' --glob '!pnpm-lock.yaml'

Repository: QueueLab/QCX

Length of output: 3233


🌐 Web query:

Clerk Supabase integration accessToken getToken template supabase deprecated native third-party authentication docs

💡 Result:

As of April 1, 2025, the previous method of integrating Clerk with Supabase using custom JWT templates and the session.getToken method is officially deprecated [1][2][3]. The recommended approach is to use Clerk as a native third-party authentication provider for Supabase [2][4][3]. This native integration is preferred for several reasons: - It eliminates the need to share your Supabase JWT secret key with Clerk [3]. - It removes the requirement to manually fetch or generate new session tokens for every Supabase API request [3]. To implement the current native integration: 1. Enable the Clerk integration within your Supabase project settings under the third-party authentication providers section [4][5]. 2. Configure your Clerk instance domain within the Supabase dashboard to allow Supabase to accept Clerk-signed session tokens [4]. 3. Follow the updated official documentation provided by Clerk and Supabase to configure your Supabase client and implement Row Level Security (RLS) policies [2][3][5]. For existing projects still using the legacy JWT template method, it is strongly advised to migrate to this new native flow to maintain security and ensure compatibility with future updates [1][3].

Citations:


🌐 Web query:

@supabase/ssr createServerClient accessToken global fetch docs

💡 Result:

The @supabase/ssr library's createServerClient function is designed specifically to manage Supabase authentication state in server-side environments by handling cookie-based session persistence [1][2]. Regarding your query on handling access tokens and global fetch configuration: 1. Access Tokens: You do not typically manually pass an access token to createServerClient. Instead, the library automatically handles token management, including session retrieval and refresh, by interacting with the cookie methods you provide during initialization [3][4][2]. When you call methods like auth.getUser or auth.getSession, the library reads the access token from the cookies, refreshes it if necessary, and keeps the session synchronized [5][4]. 2. Global Fetch: The createServerClient function accepts standard Supabase client options as its third argument [6]. Because it extends the base Supabase client, you can pass a custom fetch implementation via the global fetch option if you need to override the default behavior (e.g., for proxying or logging) [6]. Example configuration pattern: const supabase = createServerClient( process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY, { global: { fetch: (url, options) => { // Your custom fetch logic here return fetch(url, options); }, }, cookies: { // Implement getAll and setAll methods specific to your framework getAll {... }, setAll(cookiesToSet, headers) {... }, }, }); The cookie-handling methods (getAll and setAll) are mandatory for proper SSR functionality, as they ensure that refreshed tokens are persisted back to the client's browser [3][2]. Always remember to apply the headers provided in setAll to your response to prevent caching issues [2].

Citations:


🌐 Web query:

@supabase/supabase-js createClient accessToken option docs

💡 Result:

In the @supabase/supabase-js library, the accessToken option allows you to integrate third-party authentication systems (such as Auth0, Clerk, or Firebase Auth) with Supabase [1][2]. When you configure this option, you provide an asynchronous function that returns an access token or ID token (JWT) [2]. The Supabase client will use this token for its requests, effectively allowing you to bypass Supabase's built-in authentication flow [1][2]. Key behaviors and constraints: 1. Configuration: You pass it as a property within the options object of the createClient function [1][2]. const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, { accessToken: async => { // Return your third-party JWT here return 'your-jwt-token'; }, }); 2. Disabling Supabase Auth: When the accessToken option is set, the standard supabase.auth namespace becomes unavailable [1][2]. Any attempt to access properties or methods under supabase.auth (e.g., supabase.auth.getUser) will throw an error, as the client is explicitly configured for external authentication management [2]. 3. Concurrency: The function provided to accessToken may be called frequently and concurrently [2]. If your underlying authentication library does not natively support this, it is recommended to use techniques like memoization to handle the requests efficiently [2]. If your application requires both Supabase Auth and third-party authentication concurrently, you must initialize separate Supabase client instances for each auth provider [2].

Citations:


Use Clerk’s native Supabase token flow here. getToken({ template: 'supabase' }) is the legacy template path; switch this client to accessToken: async () => (await auth()).getToken() and remove the manual fetch Authorization shim.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/supabase/client.ts` around lines 8 - 32, Update the Supabase client
initialization in the surrounding client factory to use Clerk’s native
accessToken callback, returning the token from auth().getToken() without the
legacy supabase template. Remove the custom global.fetch implementation,
including manual token retrieval, Authorization header injection, and related
warning handling, while preserving the existing auth configuration.

…ening

- middleware: include _next/action routes in clerkMiddleware matcher so auth context is available for server actions (fixes resolution search)
- lib/supabase/client.ts: use getToken() without template — Supabase validates Clerk tokens directly via JWKS
- lib/supabase/browser-client.ts: same getToken() fix, minor formatting cleanup
- app/api/chats/[id]/participants/route.ts: always use 'collaborator' role, ignore untrusted input; distinguish empty vs denied
- lib/actions/chat.ts: listParticipants returns null for unauthorized instead of empty array
- lib/agents/tools/document-retrieve.tsx: add user ownership constraint to RPC query + proper type inference
- app/actions.tsx: document ingestion — add size/type validation, fix duplicate error row bug, add chunk limit
- CLERK_SUPABASE_FIX.md: update Application Integration section to reflect correct getToken() usage

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/agents/tools/document-retrieve.tsx`:
- Around line 46-55: The document retrieval query currently applies the user
ownership filter after match_document_chunks limits results, which can exclude
valid owned matches. Update the match_document_chunks invocation in the document
retrieval flow to accept and enforce userId ownership before ranking and LIMIT
5, then remove the redundant outer documents join/filter while preserving
documentId filtering.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ef7e7052-8c97-4bca-b956-0647487beeef

📥 Commits

Reviewing files that changed from the base of the PR and between 08a2a55 and de6ba96.

📒 Files selected for processing (8)
  • CLERK_SUPABASE_FIX.md
  • app/actions.tsx
  • app/api/chats/[id]/participants/route.ts
  • lib/actions/chat.ts
  • lib/agents/tools/document-retrieve.tsx
  • lib/supabase/browser-client.ts
  • lib/supabase/client.ts
  • middleware.ts
📜 Review details
🔇 Additional comments (11)
app/actions.tsx (3)

387-393: PDF ingestion still produces unusable embeddings.

application/pdf remains in ALLOWED_MIMES, but fileData.text() on Line 393 decodes the raw PDF bytes as text rather than extracting document content. Those bytes get chunked and embedded, so PDF uploads complete but yield garbage retrieval data. Either drop application/pdf from the allowlist or route PDFs through a real text extractor before chunking.


350-432: LGTM!


831-846: LGTM!

lib/agents/tools/document-retrieve.tsx (2)

13-45: LGTM!


57-63: 🩺 Stability & Availability

No change needed for db.execute shape @/lib/db is built with drizzle-orm/postgres-js, so db.execute(...) returns rows directly and the results.length check is valid.

			> Likely an incorrect or invalid review comment.
lib/supabase/browser-client.ts (1)

4-30: The global.fetch wrapper still only injects the Clerk JWT into HTTP REST calls; Realtime WebSocket subscriptions bypass this path and can fail RLS checks. This was already flagged in a prior review — the template removal partially addresses the deprecation concern, but the core accessToken recommendation remains unadopted.

lib/supabase/client.ts (1)

7-34: Same global.fetch vs accessToken concern as the browser client — already flagged in a prior review. The template removal is noted, but the server client still manually injects the Bearer header via global.fetch instead of using the accessToken callback.

middleware.ts (1)

9-12: LGTM!

lib/actions/chat.ts (2)

363-396: LGTM!


323-346: 🗄️ Data Integrity & Integration

chat_participants already enforces the needed uniqueness. onConflictDoNothing() is safe here because there’s a UNIQUE(chat_id, user_id) constraint on chat_participants.

			> Likely an incorrect or invalid review comment.
app/api/chats/[id]/participants/route.ts (1)

5-29: LGTM! The GET handler correctly maps null → 403 and [] → 200, addressing the prior review's concern about distinguishing "no collaborators" from "access denied." The POST handler correctly ignores the untrusted client role and always passes 'collaborator', addressing the prior role-validation concern. All three handlers follow a consistent auth-check → action → status-mapping pattern with proper await params for Next.js 15 async params.

Also applies to: 31-59, 61-88

Comment on lines +46 to +55
const rpcResult = await db.execute(sql`
SELECT mc.id, mc.document_id as "documentId", mc.chunk_text as "chunkText", mc.similarity::float
FROM match_document_chunks(
${JSON.stringify(embedding)}::vector,
0.1,
5,
${documentId || null}::uuid
) mc
INNER JOIN documents d ON mc.document_id = d.id AND d.user_id = ${userId}::uuid
`)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect match_document_chunks definition to confirm the LIMIT is internal
fd -e sql | xargs rg -n -A40 'function[[:space:]]+match_document_chunks|match_document_chunks\s*\('

Repository: QueueLab/QCX

Length of output: 2173


🏁 Script executed:

#!/bin/bash
sed -n '1,120p' lib/agents/tools/document-retrieve.tsx
printf '\n---\n'
sed -n '74,110p' drizzle/migrations/0005_add_documents_rag.sql

Repository: QueueLab/QCX

Length of output: 3946


Move ownership filtering into the RPC

At lib/agents/tools/document-retrieve.tsx:46-55, LIMIT 5 runs inside match_document_chunks before the INNER JOIN documents ... user_id = ... filter. On the documentId-less path, that can silently drop valid matches from the current user and return fewer than 5 results even when relevant chunks exist. Push the user constraint into match_document_chunks so ranking and limiting happen on the owned chunks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/agents/tools/document-retrieve.tsx` around lines 46 - 55, The document
retrieval query currently applies the user ownership filter after
match_document_chunks limits results, which can exclude valid owned matches.
Update the match_document_chunks invocation in the document retrieval flow to
accept and enforce userId ownership before ranking and LIMIT 5, then remove the
redundant outer documents join/filter while preserving documentId filtering.

@ngoiyaeric ngoiyaeric merged commit e773257 into main Jul 11, 2026
5 of 6 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Jul 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants