Chat with your own documents. Upload a PDF, paste text, or point it at a URL, and ask questions. Every answer cites the chunks it came from, and the model is told to say so rather than invent when the sources do not cover the question.
RAG over pgvector, end to end, in about 400 lines.
- Ingest. Extract text from a PDF, a pasted block, or a scraped web page.
- Chunk. Split on paragraph boundaries with overlap, dropping fragments.
- Embed. Voyage
voyage-3, batched, with the document input type. - Store. Postgres with pgvector, through Supabase.
- Ask. Embed the question with the query input type, pull the nearest chunks, and stream an answer grounded in them.
Both the ingest and the chat endpoints stream NDJSON, so the UI shows progress during a long embed and renders citations before the first token of the answer arrives.
| Piece | Choice |
|---|---|
| Framework | Next.js 16, App Router, node runtime |
| Embeddings | Voyage voyage-3 |
| Vector store | Supabase Postgres with pgvector |
| Answering | Anthropic claude-haiku-4-5 |
| PDF extraction | unpdf |
| Styling | Tailwind 4 |
npm install
npm run devYou need four environment variables:
| Variable | For |
|---|---|
VOYAGE_API_KEY |
Embeddings |
ANTHROPIC_API_KEY |
Answering |
SUPABASE_URL |
Vector store |
SUPABASE_ANON_KEY |
Vector store |
The database needs two tables and one RPC before anything works. The schema is in
TECHNICAL.md, including the match_chunks function the search path
calls.
app/api/ingest/route.ts extract, chunk, embed, store; streams progress
app/api/chat/route.ts retrieve, then stream a grounded answer
app/page.tsx the whole UI
lib/chunker.ts paragraph-first splitting with overlap
lib/embed.ts Voyage client, document and query modes
lib/store.ts Supabase reads and writes
lib/types.ts shared shapes, including the stream event union
TECHNICAL.md covers the chunking strategy and why it cascades through
three boundary types, why the document and query embedding calls are separate
functions, the database schema and the match_chunks RPC, the NDJSON streaming
protocol, and the known limits of the URL scraper.