Educational demo of a Retrieval-Augmented Generation (RAG) app that answers questions about evidence-based mental skills (e.g., box breathing, grounding, body scan). It retrieves relevant chunks from curated Markdown content stored in a Supabase Postgres database (with pgvector), then asks an LLM to answer strictly using those sources.
Important: This is for educational purposes only. It is not medical advice. If you’re in crisis, contact local emergency services or a crisis hotline.
- RAG flow end-to-end using Supabase + pgvector for similarity search.
- Ingestion of Markdown content into a vectorized table.
- A minimal Next.js UI that chats with an API route.
- OpenAI models for embeddings and generation.
- Data: Markdown files under
src/data/mental-skills/*.md. - Embeddings: OpenAI
text-embedding-3-small. - Vector store: Supabase Postgres +
pgvectorwith an RPC for similarity search. - Orchestration:
answerQuestion()insrc/lib/rag.tsembeds the query, retrieves top matches, and calls an LLM (gpt-4.1-mini). - API:
POST /api/rag-queryaccepts{ question: string }, returns{ answer, sources }. - UI:
src/app/page.tsxsends questions and renders answers with sources.
- Node 18+ and npm
- A Supabase project with pgvector enabled
- OpenAI API key
Create ./.env.local with the following (replace values with your own):
# Supabase (project settings → API)
NEXT_PUBLIC_SUPABASE_URL="https://YOUR-PROJECT.ref.supabase.co"
NEXT_PUBLIC_SUPABASE_ANON_KEY="YOUR-ANON-KEY"
# Service key is required for ingestion (server-side writes)
SUPABASE_SERVICE_KEY="YOUR-SERVICE-ROLE-KEY"
# OpenAI
OPENAI_API_KEY="sk-..."If you prefer, you can also put these into a plain .env file for the scripts; the app uses .env.local by default.
Run these SQL statements in Supabase SQL Editor. Adjust dimensions if you change the embedding model.
-- 1) Extensions (pgvector)
create extension if not exists vector;
-- 2) Table for chunks
create table if not exists mental_skills_chunks (
id uuid primary key default gen_random_uuid(),
title text,
source text,
chunk text,
embedding vector(1536) -- text-embedding-3-small dimension
);
-- 3) Optional: vector index for faster ANN search
create index if not exists mental_skills_chunks_embedding_idx
on mental_skills_chunks using ivfflat (embedding vector_cosine_ops)
with (lists = 100);
-- 4) RLS (recommended)
alter table mental_skills_chunks enable row level security;
-- Allow public/anon to read chunks (UI and API read via anon key)
create policy if not exists "Allow read for anon" on mental_skills_chunks
for select using (true);
-- 5) Similarity search RPC (cosine distance)
create or replace function match_mental_skills_chunks(
query_embedding vector(1536),
match_count int default 5
)
returns table (
id uuid,
title text,
source text,
chunk text,
similarity float
)
language sql stable as $$
select
m.id,
m.title,
m.source,
m.chunk,
1 - (m.embedding <=> query_embedding) as similarity
from mental_skills_chunks m
order by m.embedding <=> query_embedding
limit match_count;
$$;
-- Allow anon to execute the RPC
grant execute on function match_mental_skills_chunks(vector(1536), int) to anon;# Install deps
npm install
# Ingest content (reads Markdown, chunks, embeds, and inserts)
npm run ingest
# Optional: verify DB connectivity and a sample read
npm run db:test-read
# Start the UI
npm run devThen open http://localhost:3000 and try questions like:
- “How can I use box breathing to manage anxiety?”
- “What is a grounding exercise?”
Markdown files live here:
src/data/mental-skills/body-scan.mdsrc/data/mental-skills/box-breathing.mdsrc/data/mental-skills/grounding-54321.mdsrc/data/mental-skills/stop.mdsrc/data/mental-skills/urge-surfing.md
The ingestion script will chunk these and upsert them into mental_skills_chunks with embeddings.
src/lib/db.ts: Supabase client (anon key for reads in app).src/lib/embeddings.ts: Embedding helper using OpenAItext-embedding-3-small.src/lib/rag.ts:answerQuestion()→ embed query → Supabase RPC → callgpt-4.1-miniwith retrieved context.src/app/api/rag-query/route.ts: API to handlePOSTquestions.src/app/page.tsx: Minimal chat-style UI showing answers and sources.src/scripts/ingest.ts: Offline ingestion usingSUPABASE_SERVICE_KEY.src/scripts/test-read.ts: Quick connectivity/read test.
Local example:
curl -s -X POST http://localhost:3000/api/rag-query \
-H 'Content-Type: application/json' \
-d '{"question":"How do I use box breathing?"}'Returns JSON like:
{
"answer": "...model-generated text grounded in sources...",
"sources": [
{ "id": "...", "title": "box-breathing", "source": "box-breathing.md", "chunk": "...", "similarity": 0.87 }
]
}- Embeddings:
text-embedding-3-small(1536-dim) - Chat:
gpt-4.1-mini - The system prompt confines responses to provided context and reminds users this isn’t medical advice. Out-of-scope questions should receive a gentle deferral.
- “Vector search failed: function match_mental_skills_chunks does not exist”
- Ensure you created the RPC and granted execute to
anon.
- Ensure you created the RPC and granted execute to
- “column embedding is of type vector(1536) but expression is …”
- Confirm the embedding model dimensions match your table definition.
- No sources returned
- Verify rows exist in
mental_skills_chunksand thatnpm run ingestsucceeded.
- Verify rows exist in
- Auth/RLS issues
- Reads use the anon key. Inserts require the service role key. With RLS enabled, service role bypasses policies; anon needs a
selectpolicy.
- Reads use the anon key. Inserts require the service role key. With RLS enabled, service role bypasses policies; anon needs a
This is a demo intended for learning and experimentation. Use responsibly and at your own risk.