You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Thoroughly analyze supabase clerk configuration for complete backend intergration such that document attachments and retrieval augmented generation are possible. Multi user collaboration through real time and that the update delete functions for save chat are all functional. Perform a deep analysis for this and forsee future issues.
Convert the mock Supabase clients into real Clerk-native-third-party-auth clients so end-user requests can drive Supabase RLS, Storage, and Realtime — the missing foundation the whole ticket depends on.
Finish chat CRUD by exposing the existing delete primitive and adding rename/visibility via new server actions, a [id] API route, and per-item history UI controls.
Build a real document-attachment + RAG pipeline (Storage upload → parse/chunk/embed into new documents/document_chunks pgvector tables → new agent retrieval tool) reusing existing tool and schema patterns.
Activate multi-user collaboration by implementing chat_participants access control, re-enabling sharing, and adding RLS-gated postgres_changes Realtime subscriptions, with explicit hardening for dual-write identity consistency and concurrent-write safety.
Design ChoicesDesign Choice 1: How should end-user requests authenticate to Supabase for Storage/Realtime/RLS?
Options Considered:
Adopt Clerk's native third-party auth integration (Clerk session token accepted directly by Supabase), configure Supabase clients with an accessToken callback, and enforce access via the existing clerk_id()/is_clerk_user() RLS functions.
Keep the deprecated Clerk "Supabase JWT template" approach (mint a Supabase-signed token per request).
Continue service-role-only access and proxy all Storage/Realtime through custom server endpoints.
Chosen Option: 1
Rationale: Web search confirms Clerk deprecated JWT templates (Mar 2025) in favor of native third-party auth, which works across Data API, Storage, and Realtime with RLS and avoids sharing JWT secrets — aligning with the existing clerk_id()/is_clerk_user() RLS bridge already in migrations.
Design Choice 2: Where should uploaded-document embeddings live for RAG?
Options Considered:
Reuse messages.embedding and store document chunks as pseudo-messages.
Add dedicated documents + document_chunks tables (chunk text + vector(1536) embedding), with a retrieval RPC.
Chosen Option: 2
Rationale: A dedicated documents/chunks model keeps attachment lifecycle, chunking, and ownership distinct from chat messages and mirrors the existing pgvector customType pattern without overloading messages.
Design Choice 3: Which Realtime mechanism should power collaboration?
Options Considered:
postgres_changes subscriptions on messages/chat_participants (RLS-gated, simplest).
Broadcast/private channels with realtime.messages RLS policies.
Chosen Option: 1
Rationale: Per web search, postgres_changes adheres to existing table RLS with no extra authorization tables, matching the already-defined RLS policies; Broadcast/Presence can be layered later for cursors/typing if needed.
💡 User Tips
Regenerate the plan with different choices with @coderabbitai <feedback>.
Implementation Steps
Phase 1: Establish a Real End-User Clerk↔Supabase Auth Bridge
This foundational phase replaces the inert mock Supabase clients with real clients that carry the signed-in Clerk user's identity, so that Supabase RLS, Storage, and Realtime can authorize interactive requests (not just the service-role webhook). This unblocks every subsequent phase, since Storage uploads, RAG retrieval, and Realtime collaboration all depend on per-user Supabase authorization.
Task 1: Configure Clerk native third-party auth with Supabase
Enable Supabase to trust Clerk session tokens and document the required configuration.
Document (in a setup note alongside CLERK_SUPABASE_FIX.md) the Clerk dashboard + Supabase "Third-Party Auth (Clerk)" configuration steps, replacing the deprecated JWT-template approach.
Add any needed env vars to .env.local.example (Supabase URL/anon key already present); confirm no shared JWT secret is required under native integration.
Verify the existing clerk_id()/is_clerk_user() RLS helpers resolve correctly against the identity now carried by Clerk-issued tokens; add a migration only if the claim lookup needs adjustment for the native token's sub.
Task 2: Implement functional browser and server Supabase clients bound to Clerk
Replace the mock stubs with real, identity-aware clients.
Rewrite lib/supabase/browser-client.ts to create a @supabase/supabase-js client whose accessToken callback returns the current Clerk session token (via Clerk's client-side session), so browser Storage/Realtime calls authorize as the user.
Rewrite lib/supabase/client.ts (server) to create a client whose accessToken callback returns the Clerk token from @clerk/nextjs/server's auth(), for server-side RLS-gated reads/writes and signed-URL generation.
Preserve the existing service-role client usage in app/api/clerk/webhook/route.ts for the sync RPC (privileged path unchanged).
Task 3: Reconcile identity and connection configuration
Remove the documented env/connection inconsistencies that will otherwise cause subtle production failures.
Align .env.local.example so the Drizzle connection variable actually read by code (POSTGRES_URL) is documented consistently with DATABASE_URL.
Confirm users.clerk_user_id linkage remains authoritative so Drizzle-based ownership checks and Supabase RLS agree on the same user mapping.
🤖 Prompt for AI agents
Implement the Clerk↔Supabase end-user auth bridge that replaces the currently
inert mock Supabase clients with real, identity-aware clients, using Clerk's
native third-party auth integration (not the deprecated JWT-template approach).
- Configure Clerk dashboard + Supabase "Third-Party Auth (Clerk)" settings;
write a setup note alongside `CLERK_SUPABASE_FIX.md` documenting these steps.
- Update `.env.local.example` with any needed env vars (Supabase URL/anon key
already present); confirm no shared JWT secret is needed under native
integration.
- Verify `clerk_id()`/`is_clerk_user()` RLS helper functions correctly resolve
identity from Clerk-issued tokens; only add a new migration if the claim lookup
needs adjustment for the native token's `sub` claim.
- Rewrite `lib/supabase/browser-client.ts`: create a `@supabase/supabase-js`
client with an `accessToken` callback that returns the current Clerk client-side
session token, so browser Storage/Realtime calls authorize as the signed-in
user.
- Rewrite `lib/supabase/client.ts` (server): create a client with an
`accessToken` callback that returns the Clerk token via `@clerk/nextjs/server`'s
`auth()`, for server-side RLS-gated reads/writes and signed-URL generation.
- Keep `app/api/clerk/webhook/route.ts`'s existing service-role client and
`sync_clerk_user` RPC usage unchanged (this privileged path is separate from the
new user-bound clients).
- Align `.env.local.example` so the Drizzle connection variable actually read by
code (`POSTGRES_URL`) is documented consistently alongside `DATABASE_URL`,
removing the inconsistency.
- Confirm `users.clerk_user_id` remains the authoritative linkage so
Drizzle-based ownership checks and Supabase RLS checks agree on the same user
mapping.
Phase 2: Complete Chat CRUD (Update & Delete)
This phase makes the save-chat lifecycle fully functional by exposing the already-existing delete primitive and adding rename/visibility update, then surfacing both in the history UI. It builds directly on existing patterns (chat-db.ts, chat.ts, app/api/chats/*).
Task 1: Add server-action and DB update/delete capabilities
Expose per-chat mutations with ownership enforcement.
Add updateChat (title/visibility) to lib/actions/chat-db.ts, and add server-action wrappers deleteChat and updateChat in lib/actions/chat.ts that resolve the current user and enforce ownership, then revalidatePath('/').
Reuse the existing (currently unwired) deleteChat(id, userId) in lib/actions/chat-db.ts.
Task 2: Add per-chat API routes
Provide REST endpoints paralleling the existing bulk routes.
Create app/api/chats/[id]/route.ts supporting DELETE (single chat) and PATCH (rename/visibility), calling the new server actions and enforcing Clerk auth like app/api/chats/all/route.ts.
Task 3: Wire delete/rename affordances into the history UI
Give users per-chat controls.
Extend components/history-item.tsx (and its container components/sidebar/chat-history-client.tsx) with a per-item menu offering Delete (confirm dialog, mirroring the existing clear-history AlertDialog) and Rename (inline edit) that call the new API routes and refresh the list.
Add data-testid hooks consistent with existing test conventions in tests/sidebar.spec.ts.
🤖 Prompt for AI agents
Complete the chat CRUD surface by exposing the existing delete primitive and
adding rename/visibility update, then surfacing both in the history UI, building
directly on existing patterns in `chat-db.ts`, `chat.ts`, and `app/api/chats/*`.
- In `lib/actions/chat-db.ts`, add an `updateChat` function (title/visibility)
alongside the existing `deleteChat(id, userId)` DB primitive.
- In `lib/actions/chat.ts`, add server-action wrappers `deleteChat` and
`updateChat` that resolve the current user, enforce ownership, and call
`revalidatePath('/')` after mutation.
- Reuse the existing (currently unwired) `deleteChat(id, userId)` DB function
rather than duplicating logic.
- Create `app/api/chats/[id]/route.ts` supporting `DELETE` (single chat) and
`PATCH` (rename/visibility) HTTP methods, calling the new server actions and
enforcing Clerk auth the same way as `app/api/chats/all/route.ts`.
- Extend `components/history-item.tsx` and its container
`components/sidebar/chat-history-client.tsx` with a per-item menu offering:
Delete (with a confirmation dialog mirroring the existing clear-history
`AlertDialog` pattern) and Rename (inline edit). Both actions should call the
new API routes and refresh the chat list afterward.
- Add `data-testid` attributes consistent with the existing test conventions
found in `tests/sidebar.spec.ts`.
Phase 3: Document Attachments & RAG Pipeline
This phase turns the client-only attachment stub into a real ingestion + retrieval flow: documents are uploaded to Supabase Storage, parsed and chunked, embedded, stored in pgvector, and retrieved at query time via a new agent tool. It relies on the Phase 1 Supabase clients for Storage/RLS.
Task 1: Provision Supabase Storage and document schema
Create the persistence layer for attachments and their embeddings.
Add a private Supabase Storage bucket for chat attachments with RLS policies keyed to is_clerk_user()/owner.
Add documents (metadata: owner, chatId, storage path, mime, status) and document_chunks (chunkText, embedding vector(1536), FK to documents) tables to lib/db/schema.ts, reusing the existing pgvector customType; generate a new Drizzle migration.
Add a document-scoped vector retrieval RPC (mirroring the existing hybrid_search() pattern) or a Drizzle cosine-distance query helper.
Task 2: Implement upload + ingestion flow
Persist and process uploaded documents.
In components/chat-panel.tsx, when a non-image document is selected, upload it to Storage via the Phase 1 browser client and append a storage reference to FormData (instead of sending raw bytes for documents; keep the existing inline-image path).
In app/actions.tsxsubmit(), branch on MIME type in the existing file-handling block: for documents, create a documents row, extract text, chunk it, generate embeddings via @ai-sdk/openai, and persist chunks — keeping the image path unchanged.
Optionally offload ingestion to a new app/api/documents/route.ts for larger files, following the existing route conventions.
Task 3: Add a RAG retrieval tool to the agent
Feed retrieved document context into responses.
Add lib/schema/document-retrieve.tsx (Zod params: query + optional documentId) following the retrieve/search schema pattern.
Add lib/agents/tools/document-retrieve.tsx that embeds the query, runs the vector search, streams a UI card, and returns matched chunks; register it in lib/agents/tools/index.tsx (env-gated like search).
Add a rendering branch in app/actions.tsxgetUIStateFromAIState and a directive in the researcher system prompt (lib/agents/researcher.tsx) for when to invoke document retrieval.
🤖 Prompt for AI agents
Implement the document attachment and RAG pipeline, turning the current
client-only attachment stub into a real ingestion + retrieval flow: upload to
Supabase Storage, parse/chunk/embed into pgvector, and retrieve via a new agent
tool. This depends on the Phase 1 Supabase clients for Storage/RLS.
- Provision a private Supabase Storage bucket for chat attachments with RLS
policies keyed to `is_clerk_user()`/owner.
- Add `documents` (metadata: owner, chatId, storage path, mime, status) and
`document_chunks` (`chunkText`, `embedding vector(1536)`, FK to `documents`)
tables in `lib/db/schema.ts`, reusing the existing pgvector `customType`.
Generate a new Drizzle migration for these tables.
- Add a document-scoped vector retrieval RPC mirroring the existing
`hybrid_search()` pattern, or alternatively a Drizzle cosine-distance query
helper.
- In `components/chat-panel.tsx`, when a non-image document is selected, upload
it to Storage via the Phase 1 browser client and append a storage reference to
`FormData` instead of sending raw bytes; keep the existing inline-image path
unchanged.
- In `app/actions.tsx`'s `submit()`, branch on MIME type within the existing
file-handling block: for documents, create a `documents` row, extract text,
chunk it, generate embeddings via `@ai-sdk/openai`, and persist the chunks;
leave the image handling path unchanged.
- Optionally, offload ingestion to a new `app/api/documents/route.ts` for larger
files, following existing route conventions.
- Add `lib/schema/document-retrieve.tsx` with Zod params (query + optional
documentId), following the existing `retrieve`/`search` schema pattern.
- Add `lib/agents/tools/document-retrieve.tsx` that embeds the query, runs the
vector search, streams a UI card, and returns matched chunks. Register this tool
in `lib/agents/tools/index.tsx`, env-gated similarly to the `search` tool.
- Add a rendering branch in `app/actions.tsx`'s `getUIStateFromAIState` for the
new tool's output, and add a directive in the researcher system prompt
(`lib/agents/researcher.tsx`) describing when to invoke document retrieval.
Phase 4: Multi-User Real-Time Collaboration
This phase activates the dormant chat_participants table and layers Supabase Realtime so multiple authorized users see chat updates live. It depends on Phase 1 (Realtime authorizes via the Clerk-bound client) and Phase 2 (shared chat access control).
Task 1: Implement collaborator access control
Make chats shareable to specific users.
Add participant CRUD (add/remove/list collaborators) in lib/actions/chat.ts + a app/api/chats/[id]/participants/route.ts, writing to the existing chat_participants table.
Update chat access checks (getChat/getChatsPage in lib/actions/chat-db.ts) and the corresponding RLS policies so participants — not only owners — can read/append to shared chats.
Re-enable and reimplement components/chat-share.tsx (currently early-returns null) as an "invite collaborator / share" dialog driven by the new participant actions.
Task 2: Add Realtime synchronization to the chat view
Live-sync messages and participants across users.
In components/chat.tsx (and/or the message list), subscribe via the Phase 1 browser client to postgres_changes on messages (and chat_participants) filtered by chatId, appending/refreshing state on insert/update; unsubscribe on unmount.
Rely on postgres_changes following existing table RLS so users only receive events for chats they can access (per Supabase Realtime + RLS behavior).
Task 3: Foreseeable-issue hardening
Address the risks surfaced by the analysis.
Ensure saveChat's upsert does not clobber concurrent collaborators' messages (dedup by message id already exists via onConflictDoUpdate; verify ownership/userId stamping per message rather than per chat).
Document/guard the dual-write consistency between the Drizzle app DB and the service-role sync so a user existing in one store but not the other cannot break RLS-authorized Realtime/Storage access.
🤖 Prompt for AI agents
Implement multi-user real-time chat collaboration by activating the dormant
`chat_participants` table and layering Supabase Realtime, dependent on Phase 1's
Clerk-bound Supabase clients and Phase 2's chat access control.
- Add participant CRUD (add/remove/list collaborators) in `lib/actions/chat.ts`
and a new `app/api/chats/[id]/participants/route.ts`, writing to the existing
`chat_participants` table.
- Update chat access checks in `getChat`/`getChatsPage`
(`lib/actions/chat-db.ts`) and the corresponding RLS policies so that
participants, not just owners, can read/append to shared chats.
- Re-enable and reimplement `components/chat-share.tsx` (which currently
early-returns `null`) as an "invite collaborator / share" dialog driven by the
new participant actions.
- In `components/chat.tsx` (and/or the message list), subscribe via the Phase 1
browser client to `postgres_changes` events on `messages` (and
`chat_participants`) filtered by `chatId`; append/refresh local state on
insert/update events; ensure unsubscription on component unmount.
- Rely on `postgres_changes` following the existing table RLS policies so that
users only receive Realtime events for chats they are authorized to access.
- Harden against foreseeable issues: verify `saveChat`'s upsert does not clobber
concurrent collaborators' messages (message-id dedup already exists via
`onConflictDoUpdate`; verify ownership/`userId` stamping is done per message
rather than per chat).
- Document and guard against dual-write consistency issues between the Drizzle
app DB and the service-role sync, ensuring a user existing in one store but not
the other cannot break RLS-authorized Realtime/Storage access.
Research
This is a Next.js App Router app using Clerk for auth and a Supabase-hosted Postgres accessed two ways: (1) the app DB via Drizzle + postgres-js (lib/db/index.ts, POSTGRES_URL), where RLS is disabled and access is gated in application code via getClerkUserIdOnServer/resolveClerkUserToDbUser; and (2) a separate Supabase project reached only from app/api/clerk/webhook/route.ts using the service-role key to call a sync_clerk_user RPC.
The Supabase browser/server clients in lib/supabase/ are inert mock stubs, so no end-user request currently presents a Clerk identity to Supabase (RLS/clerk_id()/is_clerk_user() from migrations 0003/0004 therefore never fire for interactive traffic).
Chat persistence flows through the RSC submit() action in app/actions.tsx, which invokes researcher() (tool-calling via the ai SDK, tools registered in lib/agents/tools/index.tsx) and saves via onSetAIState → saveChat.
The schema (lib/db/schema.ts) already defines a pgvector messages.embedding column, a hybrid_search() RPC, and a chat_participants table — all currently unused.
Chat delete exists in the DB layer (deleteChat) but is unwired; rename/visibility update and per-chat UI affordances do not exist; document attachments are client-only (bundled into FormData, never persisted); and no Realtime code exists.
🚀 Next Steps
🤖 All AI agent prompts combined
Task: 1
Implement the Clerk↔Supabase end-user auth bridge that replaces the currently
inert mock Supabase clients with real, identity-aware clients, using Clerk's
native third-party auth integration (not the deprecated JWT-template approach).
- Configure Clerk dashboard + Supabase "Third-Party Auth (Clerk)" settings;
write a setup note alongside `CLERK_SUPABASE_FIX.md` documenting these steps.
- Update `.env.local.example` with any needed env vars (Supabase URL/anon key
already present); confirm no shared JWT secret is needed under native
integration.
- Verify `clerk_id()`/`is_clerk_user()` RLS helper functions correctly resolve
identity from Clerk-issued tokens; only add a new migration if the claim lookup
needs adjustment for the native token's `sub` claim.
- Rewrite `lib/supabase/browser-client.ts`: create a `@supabase/supabase-js`
client with an `accessToken` callback that returns the current Clerk client-side
session token, so browser Storage/Realtime calls authorize as the signed-in
user.
- Rewrite `lib/supabase/client.ts` (server): create a client with an
`accessToken` callback that returns the Clerk token via `@clerk/nextjs/server`'s
`auth()`, for server-side RLS-gated reads/writes and signed-URL generation.
- Keep `app/api/clerk/webhook/route.ts`'s existing service-role client and
`sync_clerk_user` RPC usage unchanged (this privileged path is separate from the
new user-bound clients).
- Align `.env.local.example` so the Drizzle connection variable actually read by
code (`POSTGRES_URL`) is documented consistently alongside `DATABASE_URL`,
removing the inconsistency.
- Confirm `users.clerk_user_id` remains the authoritative linkage so
Drizzle-based ownership checks and Supabase RLS checks agree on the same user
mapping.
===============================================================================
Task: 2
Complete the chat CRUD surface by exposing the existing delete primitive and
adding rename/visibility update, then surfacing both in the history UI, building
directly on existing patterns in `chat-db.ts`, `chat.ts`, and `app/api/chats/*`.
- In `lib/actions/chat-db.ts`, add an `updateChat` function (title/visibility)
alongside the existing `deleteChat(id, userId)` DB primitive.
- In `lib/actions/chat.ts`, add server-action wrappers `deleteChat` and
`updateChat` that resolve the current user, enforce ownership, and call
`revalidatePath('/')` after mutation.
- Reuse the existing (currently unwired) `deleteChat(id, userId)` DB function
rather than duplicating logic.
- Create `app/api/chats/[id]/route.ts` supporting `DELETE` (single chat) and
`PATCH` (rename/visibility) HTTP methods, calling the new server actions and
enforcing Clerk auth the same way as `app/api/chats/all/route.ts`.
- Extend `components/history-item.tsx` and its container
`components/sidebar/chat-history-client.tsx` with a per-item menu offering:
Delete (with a confirmation dialog mirroring the existing clear-history
`AlertDialog` pattern) and Rename (inline edit). Both actions should call the
new API routes and refresh the chat list afterward.
- Add `data-testid` attributes consistent with the existing test conventions
found in `tests/sidebar.spec.ts`.
===============================================================================
Task: 3
Implement the document attachment and RAG pipeline, turning the current
client-only attachment stub into a real ingestion + retrieval flow: upload to
Supabase Storage, parse/chunk/embed into pgvector, and retrieve via a new agent
tool. This depends on the Phase 1 Supabase clients for Storage/RLS.
- Provision a private Supabase Storage bucket for chat attachments with RLS
policies keyed to `is_clerk_user()`/owner.
- Add `documents` (metadata: owner, chatId, storage path, mime, status) and
`document_chunks` (`chunkText`, `embedding vector(1536)`, FK to `documents`)
tables in `lib/db/schema.ts`, reusing the existing pgvector `customType`.
Generate a new Drizzle migration for these tables.
- Add a document-scoped vector retrieval RPC mirroring the existing
`hybrid_search()` pattern, or alternatively a Drizzle cosine-distance query
helper.
- In `components/chat-panel.tsx`, when a non-image document is selected, upload
it to Storage via the Phase 1 browser client and append a storage reference to
`FormData` instead of sending raw bytes; keep the existing inline-image path
unchanged.
- In `app/actions.tsx`'s `submit()`, branch on MIME type within the existing
file-handling block: for documents, create a `documents` row, extract text,
chunk it, generate embeddings via `@ai-sdk/openai`, and persist the chunks;
leave the image handling path unchanged.
- Optionally, offload ingestion to a new `app/api/documents/route.ts` for larger
files, following existing route conventions.
- Add `lib/schema/document-retrieve.tsx` with Zod params (query + optional
documentId), following the existing `retrieve`/`search` schema pattern.
- Add `lib/agents/tools/document-retrieve.tsx` that embeds the query, runs the
vector search, streams a UI card, and returns matched chunks. Register this tool
in `lib/agents/tools/index.tsx`, env-gated similarly to the `search` tool.
- Add a rendering branch in `app/actions.tsx`'s `getUIStateFromAIState` for the
new tool's output, and add a directive in the researcher system prompt
(`lib/agents/researcher.tsx`) describing when to invoke document retrieval.
===============================================================================
Task: 4
Implement multi-user real-time chat collaboration by activating the dormant
`chat_participants` table and layering Supabase Realtime, dependent on Phase 1's
Clerk-bound Supabase clients and Phase 2's chat access control.
- Add participant CRUD (add/remove/list collaborators) in `lib/actions/chat.ts`
and a new `app/api/chats/[id]/participants/route.ts`, writing to the existing
`chat_participants` table.
- Update chat access checks in `getChat`/`getChatsPage`
(`lib/actions/chat-db.ts`) and the corresponding RLS policies so that
participants, not just owners, can read/append to shared chats.
- Re-enable and reimplement `components/chat-share.tsx` (which currently
early-returns `null`) as an "invite collaborator / share" dialog driven by the
new participant actions.
- In `components/chat.tsx` (and/or the message list), subscribe via the Phase 1
browser client to `postgres_changes` events on `messages` (and
`chat_participants`) filtered by `chatId`; append/refresh local state on
insert/update events; ensure unsubscription on component unmount.
- Rely on `postgres_changes` following the existing table RLS policies so that
users only receive Realtime events for chats they are authorized to access.
- Harden against foreseeable issues: verify `saveChat`'s upsert does not clobber
concurrent collaborators' messages (message-id dedup already exists via
`onConflictDoUpdate`; verify ownership/`userId` stamping is done per message
rather than per chat).
- Document and guard against dual-write consistency issues between the Drizzle
app DB and the service-role sync, ensuring a user existing in one store but not
the other cannot break RLS-authorized Realtime/Storage access.
💡 Iterate on the plan with:
`@coderabbitai` <feedback>
Example Feedback
- `@coderabbitai` You can skip phase 3. Add a simple unit test case for phase 2.
- `@coderabbitai` For design choice 1 go ahead with option 3 and replan.
💬 Have feedback or questions? Drop into our discord!
@ngoiyaeric Task 1 is the configuration/setup prerequisite that Task 2 depends on — it's about wiring Supabase and Clerk together at the platform level, not writing code.
Concretely, it covers:
Supabase dashboard config: Enabling Clerk as a recognized "Third-Party Auth" provider in Supabase, so Supabase's Data API, Storage, and Realtime layers know how to verify a Clerk-issued session token (validate its signature/issuer) instead of a Supabase-minted JWT.
Clerk dashboard config: Setting up Clerk's native Supabase integration so it issues tokens in the shape Supabase expects.
Env vars: Confirming .env.local.example has whatever's needed (Supabase URL/anon key) and that no shared JWT secret is required — that's a leftover artifact of the older, now-deprecated "JWT template" approach.
Verifying the RLS bridge: Checking that the existing clerk_id()/is_clerk_user() Postgres functions (from migrations 0003/0004) correctly extract the user identity from the sub claim of the native Clerk token format — this could differ subtly from the old JWT-template claims, which is why a migration might be needed if the claim shape changed.
Without this configuration step done first, Task 2's code change (adding an accessToken callback to the Supabase clients that returns a Clerk session token) would be pointless — Supabase would reject or ignore tokens from a provider it hasn't been told to trust, and RLS policies relying on clerk_id() would never resolve correctly. So Task 1 is the platform-level handshake, Task 2 is the client-side code that leverages it.
Activity
ngoiyaeric commentedon Jul 10, 2026
@coderabbitai plan
ngoiyaeric commentedon Jul 10, 2026
@coderabbitai plan
coderabbitai commentedon Jul 10, 2026
Warning
A plan generation is already in progress. Please wait for it to complete before requesting a new plan.
coderabbitai commentedon Jul 10, 2026
Coding Plan
Summary
[id]API route, and per-item history UI controls.documents/document_chunkspgvector tables → new agent retrieval tool) reusing existing tool and schema patterns.chat_participantsaccess control, re-enabling sharing, and adding RLS-gatedpostgres_changesRealtime subscriptions, with explicit hardening for dual-write identity consistency and concurrent-write safety.Design Choices
Design Choice 1: How should end-user requests authenticate to Supabase for Storage/Realtime/RLS?
Options Considered:
Chosen Option: 1
Rationale: Web search confirms Clerk deprecated JWT templates (Mar 2025) in favor of native third-party auth, which works across Data API, Storage, and Realtime with RLS and avoids sharing JWT secrets — aligning with the existing clerk_id()/is_clerk_user() RLS bridge already in migrations.
Design Choice 2: Where should uploaded-document embeddings live for RAG?
Options Considered:
Chosen Option: 2
Rationale: A dedicated documents/chunks model keeps attachment lifecycle, chunking, and ownership distinct from chat messages and mirrors the existing pgvector customType pattern without overloading messages.
Design Choice 3: Which Realtime mechanism should power collaboration?
Options Considered:
Chosen Option: 1
Rationale: Per web search, postgres_changes adheres to existing table RLS with no extra authorization tables, matching the already-defined RLS policies; Broadcast/Presence can be layered later for cursors/typing if needed.
💡 User Tips
Regenerate the plan with different choices with
@coderabbitai <feedback>.Implementation Steps
Phase 1: Establish a Real End-User Clerk↔Supabase Auth Bridge
This foundational phase replaces the inert mock Supabase clients with real clients that carry the signed-in Clerk user's identity, so that Supabase RLS, Storage, and Realtime can authorize interactive requests (not just the service-role webhook). This unblocks every subsequent phase, since Storage uploads, RAG retrieval, and Realtime collaboration all depend on per-user Supabase authorization.
Task 1: Configure Clerk native third-party auth with Supabase
Enable Supabase to trust Clerk session tokens and document the required configuration.
CLERK_SUPABASE_FIX.md) the Clerk dashboard + Supabase "Third-Party Auth (Clerk)" configuration steps, replacing the deprecated JWT-template approach..env.local.example(Supabase URL/anon key already present); confirm no shared JWT secret is required under native integration.clerk_id()/is_clerk_user()RLS helpers resolve correctly against the identity now carried by Clerk-issued tokens; add a migration only if the claim lookup needs adjustment for the native token'ssub.Task 2: Implement functional browser and server Supabase clients bound to Clerk
Replace the mock stubs with real, identity-aware clients.
lib/supabase/browser-client.tsto create a@supabase/supabase-jsclient whoseaccessTokencallback returns the current Clerk session token (via Clerk's client-side session), so browser Storage/Realtime calls authorize as the user.lib/supabase/client.ts(server) to create a client whoseaccessTokencallback returns the Clerk token from@clerk/nextjs/server'sauth(), for server-side RLS-gated reads/writes and signed-URL generation.app/api/clerk/webhook/route.tsfor the sync RPC (privileged path unchanged).Task 3: Reconcile identity and connection configuration
Remove the documented env/connection inconsistencies that will otherwise cause subtle production failures.
.env.local.exampleso the Drizzle connection variable actually read by code (POSTGRES_URL) is documented consistently withDATABASE_URL.users.clerk_user_idlinkage remains authoritative so Drizzle-based ownership checks and Supabase RLS agree on the same user mapping.🤖 Prompt for AI agents
Phase 2: Complete Chat CRUD (Update & Delete)
This phase makes the save-chat lifecycle fully functional by exposing the already-existing delete primitive and adding rename/visibility update, then surfacing both in the history UI. It builds directly on existing patterns (
chat-db.ts,chat.ts,app/api/chats/*).Task 1: Add server-action and DB update/delete capabilities
Expose per-chat mutations with ownership enforcement.
updateChat(title/visibility) tolib/actions/chat-db.ts, and add server-action wrappersdeleteChatandupdateChatinlib/actions/chat.tsthat resolve the current user and enforce ownership, thenrevalidatePath('/').deleteChat(id, userId)inlib/actions/chat-db.ts.Task 2: Add per-chat API routes
Provide REST endpoints paralleling the existing bulk routes.
app/api/chats/[id]/route.tssupportingDELETE(single chat) andPATCH(rename/visibility), calling the new server actions and enforcing Clerk auth likeapp/api/chats/all/route.ts.Task 3: Wire delete/rename affordances into the history UI
Give users per-chat controls.
components/history-item.tsx(and its containercomponents/sidebar/chat-history-client.tsx) with a per-item menu offering Delete (confirm dialog, mirroring the existing clear-historyAlertDialog) and Rename (inline edit) that call the new API routes and refresh the list.data-testidhooks consistent with existing test conventions intests/sidebar.spec.ts.🤖 Prompt for AI agents
Phase 3: Document Attachments & RAG Pipeline
This phase turns the client-only attachment stub into a real ingestion + retrieval flow: documents are uploaded to Supabase Storage, parsed and chunked, embedded, stored in pgvector, and retrieved at query time via a new agent tool. It relies on the Phase 1 Supabase clients for Storage/RLS.
Task 1: Provision Supabase Storage and document schema
Create the persistence layer for attachments and their embeddings.
is_clerk_user()/owner.documents(metadata: owner, chatId, storage path, mime, status) anddocument_chunks(chunkText,embedding vector(1536), FK todocuments) tables tolib/db/schema.ts, reusing the existing pgvectorcustomType; generate a new Drizzle migration.hybrid_search()pattern) or a Drizzle cosine-distance query helper.Task 2: Implement upload + ingestion flow
Persist and process uploaded documents.
components/chat-panel.tsx, when a non-image document is selected, upload it to Storage via the Phase 1 browser client and append a storage reference toFormData(instead of sending raw bytes for documents; keep the existing inline-image path).app/actions.tsxsubmit(), branch on MIME type in the existing file-handling block: for documents, create adocumentsrow, extract text, chunk it, generate embeddings via@ai-sdk/openai, and persist chunks — keeping the image path unchanged.app/api/documents/route.tsfor larger files, following the existing route conventions.Task 3: Add a RAG retrieval tool to the agent
Feed retrieved document context into responses.
lib/schema/document-retrieve.tsx(Zod params: query + optional documentId) following theretrieve/searchschema pattern.lib/agents/tools/document-retrieve.tsxthat embeds the query, runs the vector search, streams a UI card, and returns matched chunks; register it inlib/agents/tools/index.tsx(env-gated likesearch).app/actions.tsxgetUIStateFromAIStateand a directive in the researcher system prompt (lib/agents/researcher.tsx) for when to invoke document retrieval.🤖 Prompt for AI agents
Phase 4: Multi-User Real-Time Collaboration
This phase activates the dormant
chat_participantstable and layers Supabase Realtime so multiple authorized users see chat updates live. It depends on Phase 1 (Realtime authorizes via the Clerk-bound client) and Phase 2 (shared chat access control).Task 1: Implement collaborator access control
Make chats shareable to specific users.
lib/actions/chat.ts+ aapp/api/chats/[id]/participants/route.ts, writing to the existingchat_participantstable.getChat/getChatsPageinlib/actions/chat-db.ts) and the corresponding RLS policies so participants — not only owners — can read/append to shared chats.components/chat-share.tsx(currently early-returnsnull) as an "invite collaborator / share" dialog driven by the new participant actions.Task 2: Add Realtime synchronization to the chat view
Live-sync messages and participants across users.
components/chat.tsx(and/or the message list), subscribe via the Phase 1 browser client topostgres_changesonmessages(andchat_participants) filtered bychatId, appending/refreshing state on insert/update; unsubscribe on unmount.postgres_changesfollowing existing table RLS so users only receive events for chats they can access (per Supabase Realtime + RLS behavior).Task 3: Foreseeable-issue hardening
Address the risks surfaced by the analysis.
saveChat's upsert does not clobber concurrent collaborators' messages (dedup by message id already exists viaonConflictDoUpdate; verify ownership/userIdstamping per message rather than per chat).🤖 Prompt for AI agents
Research
This is a Next.js App Router app using Clerk for auth and a Supabase-hosted Postgres accessed two ways: (1) the app DB via Drizzle +
postgres-js(lib/db/index.ts,POSTGRES_URL), where RLS is disabled and access is gated in application code viagetClerkUserIdOnServer/resolveClerkUserToDbUser; and (2) a separate Supabase project reached only fromapp/api/clerk/webhook/route.tsusing the service-role key to call async_clerk_userRPC.The Supabase browser/server clients in
lib/supabase/are inert mock stubs, so no end-user request currently presents a Clerk identity to Supabase (RLS/clerk_id()/is_clerk_user()from migrations0003/0004therefore never fire for interactive traffic).Chat persistence flows through the RSC
submit()action inapp/actions.tsx, which invokesresearcher()(tool-calling via theaiSDK, tools registered inlib/agents/tools/index.tsx) and saves viaonSetAIState → saveChat.The schema (
lib/db/schema.ts) already defines a pgvectormessages.embeddingcolumn, ahybrid_search()RPC, and achat_participantstable — all currently unused.Chat delete exists in the DB layer (
deleteChat) but is unwired; rename/visibility update and per-chat UI affordances do not exist; document attachments are client-only (bundled intoFormData, never persisted); and no Realtime code exists.🚀 Next Steps
🤖 All AI agent prompts combined
💡 Iterate on the plan with:
`@coderabbitai` <feedback>💬 Have feedback or questions? Drop into our discord!
ngoiyaeric commentedon Jul 10, 2026
@coderabbitai what's the point of the first task
coderabbitai commentedon Jul 10, 2026
@ngoiyaericTask 1 is the configuration/setup prerequisite that Task 2 depends on — it's about wiring Supabase and Clerk together at the platform level, not writing code.Concretely, it covers:
.env.local.examplehas whatever's needed (Supabase URL/anon key) and that no shared JWT secret is required — that's a leftover artifact of the older, now-deprecated "JWT template" approach.clerk_id()/is_clerk_user()Postgres functions (from migrations0003/0004) correctly extract the user identity from thesubclaim of the native Clerk token format — this could differ subtly from the old JWT-template claims, which is why a migration might be needed if the claim shape changed.Without this configuration step done first, Task 2's code change (adding an
accessTokencallback to the Supabase clients that returns a Clerk session token) would be pointless — Supabase would reject or ignore tokens from a provider it hasn't been told to trust, and RLS policies relying onclerk_id()would never resolve correctly. So Task 1 is the platform-level handshake, Task 2 is the client-side code that leverages it.