"I want to search our internal docs with a chatbot" is one of the most common requests I hear from fellow indie developers. When that request comes up, my go-to stack is Supabase Vector + Gemini API — and Antigravity makes the whole thing surprisingly fast to build.
Since pgvector is a PostgreSQL extension, you can bolt vector storage directly onto your existing Supabase project. No extra services, no extra API keys. Combine that with Antigravity's ability to generate schema, SQL, and embedding logic on demand, and a working RAG prototype is a realistic weekend project.
This guide walks through a complete Supabase pgvector RAG implementation, using Antigravity as your coding partner throughout.
Why Choose Supabase Vector (pgvector)?
The RAG ecosystem offers plenty of vector store options — Pinecone, Weaviate, Qdrant, and more. The case for Supabase pgvector comes down to zero additional infrastructure: if you're already running Supabase, you enable the extension and add a vector column to any table. That's it.
Beyond simplicity, pgvector plays nicely with Supabase's Row Level Security. Multi-tenant RAG — where each user can only query their own documents — is straightforward with existing RLS policies, no custom middleware required.
One honest caveat: pgvector's performance at very large scales (tens of millions of vectors) won't match purpose-built vector databases. For most indie projects and internal tools, though, it's more than sufficient.
Setting Up the Environment with Antigravity
Open Antigravity's chat panel and describe your goal: "I want to build a RAG app using Supabase pgvector, TypeScript, Next.js 15 App Router, and Supabase JS v2. List the packages and setup steps."
The generated bootstrap should look roughly like this:
# Add dependencies to your Next.js project
npm install @supabase/supabase-js @ai-sdk/google ai
# .env.local
# NEXT_PUBLIC_SUPABASE_URL=https://xxxx.supabase.co
# NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
# GOOGLE_GENERATIVE_AI_API_KEY=YOUR_GEMINI_API_KEYThen head to the Supabase SQL Editor to enable pgvector and create your documents table:
-- Enable pgvector extension
create extension if not exists vector;
-- Documents table (768 dimensions = text-embedding-004 output size)
create table documents (
id uuid default gen_random_uuid() primary key,
content text not null,
metadata jsonb default '{}',
embedding vector(768),
created_at timestamptz default now()
);
-- IVFFlat index for cosine similarity search
create index on documents
using ivfflat (embedding vector_cosine_ops)
with (lists = 100);Paste this schema into Antigravity's context and ask it to generate TypeScript types and a typed Supabase client. Having the schema in context keeps subsequent code generation accurate.
Vectorizing Documents and Saving to Supabase
The heart of any RAG system is converting documents into embedding vectors and storing them. Ask Antigravity: "Write a function that embeds text using Gemini's text-embedding-004 model and upserts it to Supabase."
// lib/embeddings.ts
import { createClient } from "@supabase/supabase-js";
import { GoogleGenerativeAI } from "@google/generative-ai";
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
const genai = new GoogleGenerativeAI(process.env.GOOGLE_GENERATIVE_AI_API_KEY!);
const embeddingModel = genai.getGenerativeModel({ model: "text-embedding-004" });
// Convert text to a 768-dimensional vector and save to Supabase
export async function upsertDocument(
content: string,
metadata: Record<string, unknown> = {}
): Promise<{ id: string }> {
const result = await embeddingModel.embedContent(content);
const embedding = result.embedding.values; // 768 float values
const { data, error } = await supabase
.from("documents")
.insert({ content, metadata, embedding })
.select("id")
.single();
if (error) throw new Error(`Supabase insert error: ${error.message}`);
return { id: data.id };
}The critical detail: text-embedding-004 outputs 768 dimensions. Your table's vector(768) column must match. Switching models later means a schema migration. Worth knowing upfront.
Semantic Search and Gemini Response Generation
Once documents are stored, you need two things: a semantic search function and a generation step that feeds retrieved chunks to Gemini. Antigravity can generate both together when you provide the schema context.
First, register a PostgreSQL RPC function in Supabase:
create or replace function match_documents (
query_embedding vector(768),
match_threshold float,
match_count int
)
returns table (
id uuid,
content text,
metadata jsonb,
similarity float
)
language sql stable
as $$
select
id,
content,
metadata,
1 - (embedding <=> query_embedding) as similarity
from documents
where 1 - (embedding <=> query_embedding) > match_threshold
order by similarity desc
limit match_count;
$$;The <=> operator is pgvector's cosine distance. Subtracting from 1 converts it to a similarity score (0–1, higher is more similar).
Now wire it into a RAG query function:
// lib/rag.ts
import { generateText } from "ai";
import { google } from "@ai-sdk/google";
async function searchDocuments(query: string, topK = 5) {
const result = await embeddingModel.embedContent(query);
const queryEmbedding = result.embedding.values;
const { data, error } = await supabase.rpc("match_documents", {
query_embedding: queryEmbedding,
match_threshold: 0.7,
match_count: topK,
});
if (error) throw new Error(`Search error: ${error.message}`);
return data as Array<{ content: string; metadata: unknown; similarity: number }>;
}
export async function ragQuery(userQuestion: string): Promise<string> {
const relevantDocs = await searchDocuments(userQuestion);
if (relevantDocs.length === 0) {
return "No relevant documents found.";
}
const context = relevantDocs
.map((doc, i) => `[Document ${i + 1}]\n${doc.content}`)
.join("\n\n");
const { text } = await generateText({
model: google("gemini-2.0-flash"),
prompt: `Answer the following question based on the provided documents.\n\n${context}\n\nQuestion: ${userQuestion}`,
});
return text;
}Common Pitfalls and How to Fix Them
A few issues come up almost every time with this stack, so it's worth calling them out explicitly.
Dimension mismatch: If you change embedding models, you must update the vector column dimensions too. A vector(768) column will reject 1,536-dimensional OpenAI embeddings with a runtime error. The fix is an ALTER TABLE migration or a fresh table.
IVFFlat index timing: Creating the index before you have enough data (the recommendation is roughly sqrt(row_count) for the lists parameter) gives you little performance benefit while slowing inserts. Add the index once you have at least 1,000 rows.
RLS bypassing with service role key: If you call RPCs from a server-side route using the service_role key, RLS policies are bypassed. For user-scoped document access, use the anon key with an active user session so RLS can enforce row filtering correctly.
Antigravity handles error diagnosis well — paste the actual error message into the chat and it'll identify the cause and generate a fix. For a broader look at what you can build with Supabase and Antigravity together, check out the Supabase integration guide.
Next Steps
Supabase pgvector gives you a low-friction entry point into production RAG — no new services to manage, no additional billing to track. Start by ingesting 10–20 documents and testing search quality with real queries before scaling up.
Once you're happy with retrieval accuracy, the next meaningful improvement is chunk size and overlap tuning. The RAG pipeline vector search guide covers those design decisions in depth.
Swapping generateText for streamText from the Vercel AI SDK adds streaming responses to your chat UI with minimal code changes — a small upgrade that makes a big difference in perceived responsiveness.