Building a Model-Agnostic Memory Layer with Supabase, MCP, and Slack

A Postgres database with vector embeddings, accessible via MCP from any AI. Type a thought in Slack, and seconds later it's searchable by meaning from Claude, ChatGPT, or whatever ships next month.

Building a Model-Agnostic Memory Layer with Supabase, MCP, and Slack

Every AI conversation starts from zero. Claude doesn't know what you told ChatGPT. ChatGPT doesn't follow you into Cursor. You're burning your best thinking on context transfer instead of actual work.

I solved this with what I'm calling an Open Brain: a Postgres database with vector embeddings, accessible via MCP from any AI client. Type a thought in Slack, and seconds later it's embedded, classified, and searchable by meaning from Claude Code, Claude Desktop, ChatGPT, or whatever ships next month.

The architecture is three pieces: a Supabase Postgres database with pgvector, a Slack webhook that captures thoughts, and an MCP server that lets any AI search or write to the brain. Total cost: roughly $0.10–0.30 a month.

Why Not Just Use Platform Memory?

Claude has memory. ChatGPT has memory. But these are walled gardens: five sticky notes on five separate desks. When you switch tools, you lose everything. Worse, the accumulated context creates lock-in: you've spent months building history with one tool, and trying something new means starting over.

MCP changes this equation. One protocol that Claude, ChatGPT, Cursor, and VS Code Copilot all speak. One database they all query. Zero switching cost.

The Architecture

Slack #capture → ingest-thought Edge Function → Supabase Postgres (pgvector)
                                                        ↑
Any AI Client → open-brain-mcp Edge Function (MCP) ─────┘

Capture: A Slack message hits a Supabase Edge Function. The function generates a 1536-dimensional vector embedding via OpenRouter's text-embedding-3-small and extracts structured metadata (type, topics, people, action items) via gpt-4o-mini, both in parallel. Everything gets stored as a single row in Postgres with a pgvector HNSW index. A threaded Slack reply confirms what was captured.

Retrieval: An MCP server (another Edge Function) exposes four tools: semantic search, list recent, stats, and capture. Any MCP-compatible client connects with a URL and an access key. Search works by embedding the query and running cosine similarity against every stored thought. "Career changes" matches "thinking about leaving her job" even with zero keyword overlap.

What I Hardened

The original guide from Nate B. Jones gets you running fast. But some security gaps needed closing:

Slack signature verification. The original ingest endpoint was completely open: anyone who found the URL could POST fake thoughts and drain your OpenRouter credits. I added HMAC-SHA256 verification using Slack's signing secret, with a 5-minute timestamp window for replay protection.

Deduplication. Slack retries webhook delivery if it doesn't get a 200 response within 3 seconds. Since embedding generation takes 4-5 seconds, the original code always got retried, creating duplicate rows. Fix: return 200 immediately, process asynchronously via EdgeRuntime.waitUntil(), and enforce a unique constraint on slack_event_id as a belt-and-suspenders measure.

Slack markup cleaning. Raw Slack messages include <@U12345> user mentions, <url|label> hyperlinks, and :emoji: codes. This noise degrades embedding quality. A clean semantic search query won't reliably match a messily-encoded note. The function strips markup before embedding while preserving the original text for storage.

Accept header injection. Claude Desktop uses mcp-remote as a bridge, which doesn't send the Accept: text/event-stream header that the MCP transport requires. Without a workaround, Claude Desktop silently gets 406 Not Acceptable. The server injects the correct header before passing to the transport layer.

The Four MCP Tools

search_thoughts: Semantic search by meaning. Threshold and result count are configurable.

list_thoughts: Browse recent captures with optional filters: type (observation, task, idea, reference, person_note), topic, person mentioned, or date range.

thought_stats. Aggregate view: total count, type distribution, top topics, most-mentioned people.

capture_thought: Write directly to the brain from any AI client. No Slack needed. This is what makes the Memory Migration prompt work: you can sit in Claude and tell it to save insights directly.

What It Looks Like in Practice

I seeded my brain with 21 thoughts: professional context, tech stack, active projects, engineering philosophy, deployment patterns, and a dentist appointment. Now when I open any AI, it can query this context:

"Search my brain for notes about Kubernetes deployment" → finds my homelab architecture and life-hub deployment workflow.
"What action items do I have?" → surfaces the dentist and eye doctor appointments.
"Who have I mentioned?" → shows Steven Naviaux, Marcus Aurelius, Sister Miriam, Coach Carter (my AI council advisors).

The advantage compounds. Every thought captured makes the next search smarter. Every decision logged becomes retrievable context for every AI I'll ever use.

The Stack

ComponentTechnology
DatabaseSupabase Postgres + pgvector (HNSW index)
EmbeddingsOpenRouter → text-embedding-3-small (1536 dimensions)
Metadata extractionOpenRouter → gpt-4o-mini
Capture interfaceSlack webhook → Supabase Edge Function (Deno)
Retrieval interfaceMCP server → Supabase Edge Function (Hono + MCP SDK)
AuthHMAC-SHA256 (Slack), brain key header (MCP)
Cost~$0.10–0.30/month on free tiers

What I'd Change Next

The current metadata extraction isn't perfect: gpt-4o-mini makes its best guess with limited context, and sometimes misclassifies a thought or misses a name. It doesn't matter much because the embeddings handle the heavy lifting for search, but better prompting or a fine-tuned classifier would improve the structured filtering.

I'm also considering feeding Open Brain data into my life-hub dashboard. The systems serve different purposes (raw capture vs. structured life data), but cross-referencing them could surface patterns neither catches alone.

The code is on GitHub: snaviaux/OpenBrain.