A full-stack web app for uploading, analyzing, summarizing, and collaborating on PDF contracts. Built with Google Gemini 3.6 Flash, pgvector, Supabase RLS, and Supabase Realtime.
- PDF validation and processing: Uploads are checked for
application/pdfMIME type and a.pdfextension, on both the drag-and-drop frontend and the backend Multer middleware, then run through text extraction, sliding-window chunking, embedding, and an automatic Gemini summary. - Streaming RAG Q&A: Token-by-token answers over Server-Sent Events (SSE), grounded in the uploaded PDF.
- Hybrid search: Filename match or vector similarity search (e.g. "employment contract," "liability cap").
- Collaborative comments: Multi-threaded, synced live across open tabs via Supabase Realtime.
- Permissions and share links: Owners generate public links or invite collaborators; comments show owner/collaborator badges.
- Guest support: Guests on a share link can comment after giving a name (saved to
localStorage); logged-in users skip that prompt. - Auth and recovery: Supabase Auth, bcrypt password hashing, JWT session cookies, password reset at
/forgot-password. - Responsive UI: Next.js 16, Tailwind CSS, custom styling across mobile, tablet, and desktop.
┌───────────────────────────┐
│ Next.js 16 Frontend │
│ Tailwind CSS / Lucide │
└─────────────┬─────────────┘
│
REST / SSE Stream
│
▼
┌───────────────────────────┐
│ Express.js Backend (Node) │
│ TypeScript Services │
└──────┬─────────────┬──────┘
│ │
Google GenAI SDK │ │ Supabase Client
(Gemini 3.6 Flash) │ │ (Postgres & Storage)
▼ ▼
┌────────────┐ ┌─────────────┐
│ Gemini AI │ │ Supabase DB │
│ Models API │ │ pgvector │
└────────────┘ └─────────────┘
- Frontend: Next.js 16 (App Router, Turbopack), React 19, Tailwind CSS, Lucide React,
react-pdf,react-markdown. - Backend: Node.js, Express.js, TypeScript,
@google/genai,@supabase/supabase-admin. - Database and storage: Supabase PostgreSQL (
pgvector, HNSW indexes), Supabase Auth, Supabase Storage (pdfsbucket).
- Node.js v18+ and npm
- A Supabase project with
pgvectorenabled - A Google Gemini API key
Backend (backend/.env):
PORT=4000
SUPABASE_URL=https://your-supabase-project.supabase.co
SUPABASE_SERVICE_ROLE_KEY=your-supabase-service-role-key
GEMINI_API_KEY=your-gemini-api-key
GEMINI_MODEL=gemini-3.6-flash
EMBEDDING_MODEL=gemini-embedding-001
FRONTEND_URL=http://localhost:3000Frontend (frontend/.env.local):
NEXT_PUBLIC_SUPABASE_URL=https://your-supabase-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-supabase-anon-key
NEXT_PUBLIC_BACKEND_URL=http://localhost:4000Run supabase/schema_combined.sql in the Supabase SQL editor to set up:
- Extensions:
vector,pgcrypto - Tables:
pdfs,pdf_chunks,comments,pdf_shares,pdf_collaborators,chat_messages - Storage bucket:
pdfs(public view policies enabled) - RPCs:
match_pdf_chunks,search_user_pdfs
Note
Supabase Email Verification Note
By default, Supabase Auth may enforce email verification for new account signups.
- For Testing & Demonstration: In your Supabase Dashboard, navigate to Authentication ➔ Providers ➔ Email and toggle off "Confirm email". This enables instant account sign-up and immediate login without waiting for an email verification link.
- For Production with Email Verification: If email confirmation is enabled, ensure your Site URL and Redirect URLs in Supabase Auth Settings include
http://localhost:3000(for local dev) or your deployed frontend domain (e.g.https://your-app.vercel.app).
Backend:
cd backend
npm install
npm run dev
# Server listening on http://localhost:4000Frontend:
cd frontend
npm install
npm run dev
# App running on http://localhost:3000- Document anchor (
pdfId): On upload, PostgreSQL assigns a UUID v4 primary key (pdfs.id) that anchorspdf_chunks,comments,pdf_collaborators,chat_messages, and the storage pathpdfs/<owner_id>/<rand>.pdf. Owners manage private controls at/pdf/[id]. - Share tokens (
share_token): Owners generate a cryptographically random 32-character hex token (ShareModal.tsx, viacreateOrGetShareTokeninpdf.controller.ts), stored inpdf_shares. - Guest resolution: Visiting
/share/[token]resolves the token to itspdf_idand a signed view URL. Guest requests carry anx-share-tokenheader, checked byresolveShareOrAuthmiddleware, without exposing internal database IDs.
The app implements role-based access control for document access via share links and email invitations:
| User access level | Access path | Commenting | Badge on comments |
|---|---|---|---|
| Document owner | Document upload (/pdf/[id]) |
Unrestricted | [ShieldCheck] Author Name (Owner) (indigo badge) |
| Invited collaborator | Email invitation | Unrestricted | [User] Author Name (Collaborator) (emerald badge) |
| Registered user via share link | Public share link (/share/[token]) |
Unrestricted, no guest prompt | [User] Author Name (plain user badge) |
| Unregistered guest via share link | Public share link (/share/[token]) |
Prompted for a guest name, saved in localStorage |
[User] Guest Name [Unregistered Guest] (amber badge) |
Note
Identity-Based vs. Capability-Based Authorization The application balances two authorization paradigms:
- Identity-Based Auth (Private Owner Routes):
/pdf/[id]routes enforce strict Supabase Auth JWT identity checks (auth.uid() = owner_id). Guessing/pdf/123returns401 Unauthorized. - Capability-Based Auth (Public Share Links):
/share/[token]implements capability security (similar to Figma, Google Drive, or Dropbox share links). Holding a valid, cryptographically unguessable token (encode(gen_random_bytes(16), 'hex')) grants view, comment, and chat capabilities for that specific PDF only. Reusing tokens across PDFs or attempting sequential ID enumeration is impossible.
- Unguessable Cryptographic Tokens: Share tokens are generated server-side using secure random bytes (
gen_random_bytes(16)), ensuring links cannot be brute-forced or guessed. - Guest Identity Handshake: Unauthenticated guests accessing a share link are prompted for a display name before commenting (
CommentSidebar.tsx). The name is stored in browserlocalStorage(guestSession.ts) and rendered with a distinctUnregistered Guestamber badge tag to clearly differentiate guest comments from verified owner/collaborator comments. - Scoped LLM Chat Endpoint: The streaming AI chat endpoint (
/api/pdfs/:id/chat/stream) is strictly scoped to the target document. It requires a validx-share-tokenor user JWT validated byresolveShareOrAuth.tsand queries vector embeddings filtered strictly bytarget_pdf_id—preventing it from acting as an arbitrary open LLM proxy. - Residual Risks & Future Production Mitigations:
- Link Revocation & Expiration:
pdf_sharesincludes anexpires_atcolumn structure allowing owners to configure time-bound link validity or revoke share access. - LLM Cost & Rate Limiting: Heavy requests can be capped using IP/token rate-limiting middleware (e.g. max 10 chat requests per minute per share token) to prevent token exhaustion.
- Password Reset Integration: Self-service password recovery (
resetPasswordForEmail()) is UI-structured at/forgot-passwordand would be fully integrated with custom SMTP mailer services given more time.
- Link Revocation & Expiration:
- Text generation and summarization:
gemini-3.6-flashvia the@google/genaiSDK, chosen for fast inference and a large context window. - Embeddings:
gemini-embedding-001, configured for an explicit 768-dimension output (outputDimensionality: 768).
The RAG prompt (chat.service.ts) sets a few rules: answer only from retrieved PDF excerpts, no outside knowledge, to keep hallucinations down; if nothing relevant is found, reply "I couldn't find that in this document"; and the last 5 turns (recentTurns) are injected into the system prompt so the model can resolve follow-ups like "what about the second clause?"
Important
Long PDF Ingestion, Summarization & RAG Strategy
Handling long documents (50+ page contracts or manuals) efficiently requires a multi-stage pipeline:
- Format validation: MIME type and
.pdfextension checked client-side (UploadDropzone.tsx) and server-side (upload.ts). - Text extraction: PDF buffers parsed asynchronously with
@cyber2024/pdf-parse. - Sliding-window chunking: Text split into ~500-token chunks with a 50-token overlap (
chunking.service.ts) so sentences don't get cut off at boundaries. - Vector storage: Each chunk embedded into a 768-dim vector via
gemini-embedding-001, stored inpdf_chunksviapgvector. - Vector search (RPC): Queries embedded on the fly and matched against
pdf_chunksby cosine distance (match_pdf_chunksRPC), returning the top 6 passages. - Background processing: Embedding and summarization run in background promises, so the upload response returns immediately instead of waiting on them.
The platform runs on Supabase PostgreSQL with pgvector, anchored around a central document UUID (pdfId) with cascading integrity (ON DELETE CASCADE).
| Table | Keys and constraints | Purpose |
|---|---|---|
public.pdfs |
id (UUID v4), owner_id → auth.users(id) |
Uploaded PDF metadata: filename, storage_path, full_text, summary, upload_date. |
public.pdf_chunks |
id (UUID v4), pdf_id → pdfs(id) |
Passage text (content, chunk_index, token_count) and embedding vector(768), indexed via HNSW (pdf_chunks_embedding_hnsw_idx). |
public.comments |
id (UUID v4), pdf_id → pdfs(id), parent_comment_id → comments(id) |
Discussion threads and nested replies, with authenticated author_id → auth.users(id) or a guest_name. |
public.pdf_shares |
id (UUID v4), pdf_id (UNIQUE) → pdfs(id) |
One-to-one share link access: share_token UNIQUE, permission, expires_at. |
public.pdf_collaborators |
id (UUID v4), pdf_id → pdfs(id), user_id → auth.users(id) |
Invited collaborators and their role (viewer, commenter, editor). |
public.chat_messages |
id (UUID v4), pdf_id → pdfs(id) |
Multi-turn chat history: session_id, role (user/assistant), content. |
- RLS is enabled on every public table (
ALTER TABLE ... ENABLE ROW LEVEL SECURITY). - For view, comment, and chat operations, policies check either document ownership (
auth.uid() = owner_id) or collaborator membership inpdf_collaborators. pdf_sharesallowsSELECTfor public token resolution; guest requests carry anx-share-tokenheader, validated by backend middleware (resolveShareOrAuth.ts).
Ranks pdf_chunks by cosine distance (1 - (c.embedding <=> query_embedding)) and returns the top 6 passages for AI context grounding (embeding.service.ts):
CREATE OR REPLACE FUNCTION match_pdf_chunks(
query_embedding vector(768), target_pdf_id UUID, match_count INT DEFAULT 6
)
RETURNS TABLE (id UUID, pdf_id UUID, chunk_index INT, content TEXT, similarity FLOAT)
LANGUAGE plpgsql AS $$
BEGIN
RETURN QUERY
SELECT c.id, c.pdf_id, c.chunk_index, c.content,
1 - (c.embedding <=> query_embedding) AS similarity
FROM pdf_chunks c WHERE c.pdf_id = target_pdf_id
ORDER BY c.embedding <=> query_embedding ASC LIMIT match_count;
END; $$;Aggregates the maximum chunk similarity per document for a given user (search.controller.ts):
CREATE OR REPLACE FUNCTION search_user_pdfs(
query_embedding vector(768), user_id UUID, match_count INT DEFAULT 10
)
RETURNS TABLE (id UUID, filename TEXT, summary TEXT, max_similarity FLOAT)
LANGUAGE plpgsql AS $$
BEGIN
RETURN QUERY
SELECT p.id, p.filename, p.summary,
MAX(1 - (c.embedding <=> query_embedding)) AS max_similarity
FROM pdfs p JOIN pdf_chunks c ON c.pdf_id = p.id
WHERE p.owner_id = user_id
GROUP BY p.id, p.filename, p.summary
ORDER BY max_similarity DESC LIMIT match_count;
END; $$;Covers Supabase Auth, the pdfs storage bucket, PostgreSQL with Realtime sync, pgvector embeddings, and the RAG chat pipeline with SSE streaming.
- Password storage: Credentials and password hashes live in PostgreSQL's private
auth.usersschema, hashed with bcrypt (cost factor 10). Never stored in application databases or logs. - JWT sessions: HMAC-SHA256 signed JWT access tokens (1-hour lifespan) passed via
Authorization: Bearer, paired with HTTP-only refresh cookies handled by Next.js SSR middleware (middleware.ts). - Recovery:
signUp()email redirection (emailRedirectTo) and single-use password reset tokens (resetPasswordForEmail()) inforgot-password/page.tsx.
- Raw PDFs upload to the private
pdfsbucket atpdfs/<owner_id>/<rand>_<timestamp>.pdf(storage.service.ts). - Access goes through short-lived signed URLs (
supabaseAdmin.storage.from("pdfs").createSignedUrl(filepath, 3600)), so the viewer can render documents without exposing public bucket access.
- Tables
public.pdfs,public.comments,public.pdf_collaborators,public.pdf_shares, andpublic.chat_messagesare all anchored to the document UUID withON DELETE CASCADE. - Supabase Realtime channels (
comments-realtime-${pdfId}) listen forINSERT/UPDATE/DELETEevents and push comment updates to all open tabs instantly (CommentSidebar.tsx).
- Chunks from sliding-window extraction are embedded via
gemini-embedding-001and stored inpdf_chunks.embedding(vector(768)). - An HNSW cosine-distance index (
pdf_chunks_embedding_hnsw_idx) speeds up approximate nearest-neighbor lookups. - Two RPCs handle retrieval:
match_pdf_chunksranks by cosine distance (1 - (embedding <=> query_embedding)) and returns the top 6 passages for RAG grounding (embeding.service.ts);search_user_pdfsaggregates max similarity across chunks for semantic search over a user's PDFs (search.controller.ts).
Chat answers are grounded in the current PDF, with multi-turn memory and word-by-word streaming over SSE.
Flow:
[User Input in UI]
│
▼
1. ChatPanel.tsx (POST /api/pdfs/:id/chat/stream)
│
▼
2. resolveShareOrAuth.ts (Validates User JWT or x-share-token)
│
▼
3. chat.controller.ts (Sets SSE Headers: text/event-stream)
│
▼
4. chat.service.ts ───► Save User Turn (chat_messages Table)
│
├──► Fetch Recent History (Last 5 Turns from chat_messages)
│
├──► retriveRelevantChunks() (gemini-embedding-001 + match_pdf_chunks RPC)
│
├──► buildRagPrompt() (Injects Context + Memory + System Grounding Rules)
│
└──► ai.models.generateContentStream() (gemini-3.6-flash Model API)
│
▼ (Pipes SSE data: {"text": "..."} tokens)
5. ChatPanel.tsx (Accumulates tokens & renders Markdown + Auto-Scrolls)
Key components:
| Layer / file | Function / handler | Purpose |
|---|---|---|
UI stream reader (ChatPanel.tsx) |
handleSendMessage() |
Reads SSE chunks via getReader(), updates token state, auto-scrolls. |
Auth guard (resolveShareOrAuth.ts) |
Middleware | Validates Supabase Auth JWT or x-share-token. |
SSE controller (chat.controller.ts) |
streamQuestion() |
Sets SSE headers (text/event-stream) and starts the streaming pipeline. |
RAG orchestrator (chat.service.ts) |
processStreamChatQuestion() |
Coordinates history, vector retrieval, grounded prompt, and Gemini streaming. |
History manager (chat.service.ts) |
getRecentChatTurns() |
Retrieves the last 5 turns from chat_messages for conversation memory. |
Vector retriever (embeding.service.ts) |
retriveRelevantChunks() |
Embeds the query and calls match_pdf_chunks for the top 6 passages. |
Prompt builder (chat.service.ts) |
buildRagPrompt() |
Assembles system rules, document context, and chat history into one prompt. |
System prompt, in chat.service.ts:
You are an expert AI Legal & Document Assistant grounded strictly in the provided PDF context.
--- STRICT GROUNDING INSTRUCTIONS ---
1. Answer the user's question ONLY using the facts contained within the provided CONTEXT.
2. Do NOT extrapolate, speculate, or use external knowledge.
3. If the answer cannot be found in the provided CONTEXT, clearly respond:
"I couldn't find information about that in this document."
4. Format your answer in clean Markdown with concise bullet points where appropriate.
--- CONTEXT FROM DOCUMENT ---
[Chunk 1 text...]
[Chunk 2 text...]
--- RECENT CONVERSATION HISTORY ---
User: What is the termination notice period?
Assistant: The termination notice period is 30 business days as stated in Clause 14.2.
--- USER QUESTION ---
What about the penalty fees for early termination?
Persistence (chat_messages), defined in supabase/schema_combined.sql:
CREATE TABLE public.chat_messages (
id uuid PRIMARY KEY DEFAULT gen_random_policy_uuid(),
pdf_id uuid REFERENCES public.pdfs(id) ON DELETE CASCADE,
session_id text NOT NULL,
role text NOT NULL CHECK (role IN ('user', 'assistant')),
content text NOT NULL,
created_at timestamptz DEFAULT now()
);
-- Fast lookup index for document session history
CREATE INDEX if not exists idx_chat_messages_pdf_session
ON public.chat_messages(pdf_id, session_id, created_at);- Gemini embedding model deprecation:
models/text-embedding-004on v1beta started returning HTTP 404 errors. Fixed by switching togemini-embedding-001with an explicit 768-dimension output, matching thevector(768)schema. - PDF canvas resize flicker: The standard
react-pdfcanvas triggered continuousResizeObserverloops, flickering on window resize. Fixed with a debounced resize listener (80ms delay, 8px threshold) and fixed container overflow scrolling inPdfViewer.tsx. - Guest vs. authenticated handshake: Share links were prompting already-logged-in users for a guest name. Fixed by adding a session check (
supabase.auth.getUser()) inCommentSidebar.tsx: registered users post underauthor_id, unauthenticated guests still get the name prompt vialocalStorage. - Chat auto-scroll:
scrollIntoView({ behavior: "smooth" })kept getting interrupted by frequent streaming updates. Fixed by settingscrollTop = scrollHeightdirectly on a DOM ref (chatContainerRef) inChatPanel.tsxduring token generation. - Self-Service Password Recovery Flow: While the UI page structure (
/forgot-password) and SupabaseresetPasswordForEmail()handlers exist, configuring custom SMTP mail server credentials for outbound password reset emails was deprioritized due to the 4-day timeline. Given more time, full production SMTP dispatch and password update callbacks would be fully connected.
The supabase directory has two paths into the schema: a single consolidated file for fast setup, and ordered migration files for tracking incremental changes.
supabase/
├── schema_combined.sql # Consolidated multi-table schema, RLS policies & RPC functions
└── migrations/
├── 0001_extensions.sql # pgvector & uuid-ossp PostgreSQL extensions
├── 0002_tables.sql # Relational tables & HNSW vector index
├── 0003_rls_policies.sql # Row-Level Security (RLS) policies
├── 0004_storage_bucket.sql # Private Supabase Storage bucket initialization
├── 0005_rpc_match.sql # match_pdf_chunks RPC for RAG passage retrieval
├── 0006_alter_pdfs_table.sql # Schema migration altering pdfs table columns
├── 0007_alter_comments_table.sql # Schema migration altering comments table columns
└── 0008_rpc_search_user_pdfs.sql # search_user_pdfs RPC for global semantic search
Express.js and TypeScript, split into routes → middleware → controllers → services → config.
backend/
└── src/
├── index.ts # Entry point: imports server.ts, starts the Express server on PORT 4000
├── server.ts # Configures CORS, body parsers (json, urlencoded), registers routes under /api, mounts error handlers
├── config/
│ ├── gemini.ts # Instantiates the GoogleGenAI client with GEMINI_API_KEY; exports GEMINI_MODEL and EMBEDDING_MODEL constants
│ └── supabase.ts # Initializes supabaseAdmin with SUPABASE_SERVICE_ROLE_KEY for server-side DB/storage ops that bypass RLS
├── controllers/
│ ├── chat.controller.ts # Exposes askQuestion, streamQuestion (SSE), getChatHistory. Resolves guest vs. user session IDs
│ ├── collaborator.controller.ts # Exposes addCollaborator, listCollaborators, removeCollaborator
│ ├── comment.controller.ts # Exposes listComments, addComment; resolves author metadata (Owner, Collaborator, Guest)
│ ├── pdf.controller.ts # Handles uploadPdf, listPdf, getPdfDetails, createOrGetShareToken, getSharedPdfLookup; triggers background extraction/chunking/embedding/summary
│ └── search.controller.ts # Exposes semanticSearch; generates query embeddings, calls search_user_pdfs RPC with a text fallback
├── middleware/
│ ├── errorHandler.ts # Catches unhandled errors, formats JSON error responses with proper status codes
│ ├── requireAuth.ts # Validates Authorization: Bearer <token> JWTs via Supabase Auth, attaches req.user
│ ├── resolveShareOrAuth.ts # Permits access with either a valid user JWT or a valid x-share-token
│ └── upload.ts # Configures multer memory storage for single-file PDF uploads up to 25MB
├── routes/
│ ├── chat.routes.ts # Maps /api/pdfs/:id/chat, /:id/chat/stream, /:id/chat/history
│ ├── collaborator.routes.ts # Maps /api/pdfs/:id/collaborators and /:id/collaborators/:userId
│ ├── comment.routes.ts # Maps /api/pdfs/:id/comments
│ ├── pdf.routes.ts # Maps /api/pdfs/upload, /api/pdfs, /:id, /:id/share, /shared-lookup
│ └── search.routes.ts # Maps /api/search/semantic
├── services/
│ ├── chat.service.ts # Vector retrieval (retriveRelevantChunks), builds grounded RAG prompts with memory, streams tokens via SSE
│ ├── chunking.service.ts # Sliding-window chunking (500 tokens, 50-token overlap)
│ ├── embeding.service.ts # Calls gemini-embedding-001 (768 dims), stores embeddings in pdf_chunks, runs similarity queries
│ ├── pdfExtraction.service.ts # Converts PDF buffers to text with @cyber2024/pdf-parse
│ ├── storage.service.ts # Uploads PDFs to the Supabase pdfs bucket, generates signed view URLs
│ └── summary.service.ts # Sends full PDF text to gemini-3.6-flash for an executive summary on upload
└── types/
└── index.ts # Defines AuthRequest and other Express type extensions
Next.js 16 (App Router) and Tailwind CSS, organized into pages, components, and helper libraries.
frontend/
└── src/
├── middleware.ts # Runs updateSession for route protection and cookie refreshing
├── app/
│ ├── globals.css # Tailwind import, glassmorphism utilities (glass-panel, glass-card, glass-input), animations
│ ├── layout.tsx # Root HTML structure and font config
│ ├── page.tsx # / redirects to /dashboard
│ ├── (auth)/
│ │ ├── login/page.tsx # Login route
│ │ ├── signup/page.tsx # Signup route
│ │ └── forgot-password/page.tsx # Password reset route
│ ├── (dashboard)/
│ │ ├── layout.tsx # Sticky header nav, logo, profile display, sign-out button
│ │ └── dashboard/page.tsx # Upload dropzone, filename/semantic search bar, grid of PDF cards
│ ├── pdf/[id]/
│ │ └── page.tsx # Owner workspace: 3-panel layout (viewer, comments, AI chat), Share/Summary modals, mobile tab switcher
│ └── share/[token]/
│ └── page.tsx # Public guest workspace for /share/[token]: viewing, guest comments, AI chat
├── components/
│ ├── auth/
│ │ ├── LoginForm.tsx # Email/password fields, loading state, error messaging, Supabase auth
│ │ └── SignupForm.tsx # Registration form with client-side validation
│ ├── dashboard/
│ │ ├── PdfCard.tsx # Title, timestamp, 3-line summary preview, "View Full" trigger, workspace link
│ │ ├── SearchBar.tsx # Filename filtering plus AI semantic search toggle
│ │ └── UploadDropzone.tsx # Drag-and-drop upload: validation, progress, background-processing notices
│ ├── pdf-workspace/
│ │ ├── ChatPanel.tsx # SSE token streaming, Markdown rendering, auto-scroll pinning
│ │ ├── CommentSidebar.tsx # Multi-threaded discussion, guest name prompts, Realtime sync, 4-second fallback polling
│ │ ├── CommentThread.tsx # Role badges (Owner, Collaborator, Guest), timestamps, nested replies
│ │ ├── PdfViewer.tsx # react-pdf canvas viewer: page nav, zoom, debounced resizing
│ │ ├── ShareModal.tsx # Generates share links, invites collaborators by email
│ │ └── SummaryModal.tsx # Full Markdown AI summary with copy-to-clipboard
│ └── ui/
│ ├── Button.tsx # Variants (primary, secondary, outline, ghost), sizes, loading spinners
│ └── Input.tsx # Styled text input
└── lib/
├── api.ts # fetchApi wrapper attaching the Supabase JWT as Authorization: Bearer <token>
├── guestSession.ts # Manages guest session IDs (guest_<rand>_<time>) and names in localStorage
├── types.ts # Shared types: PdfDocument, CommentItem, ChatMessage, Collaborator
└── supabase/
├── client.ts # Browser client singleton (createBrowserClient)
├── middleware.ts # Refreshes expired auth cookies, redirects unauthenticated users to /login
└── server.ts # Server client helper (createServerClient) for Server Components/Actions