Open Brain (AI Memory)#
Open Brain is an optional self-hosted Supabase stack that provides persistent AI memory accessible via MCP (Model Context Protocol). It lets Claude.ai (or any MCP-compatible tool) capture, search, and recall thoughts across conversations.
Note
This feature requires an x86/amd64 node in your cluster. The Supabase container images do not reliably support ARM64.
Prerequisites#
An x86/amd64 node in your cluster (any node with
kubernetes.io/arch: amd64)An NFS export for database backups (used by the daily/weekly backup CronJobs — the Supabase database itself runs on a static
local-nvmePV on nuc2)Cloudflare Tunnel configured (for external access)
OAuth2 proxy configured (for Studio dashboard access)
1 – Enable Supabase in values.yaml#
Edit kubernetes-services/values.yaml and add at the end:
# Supabase self-hosted stack for Open Brain AI memory.
enable_supabase: true
supabase:
nfs:
server: 192.168.1.3 # your NFS server IP
path: /bigdisk/OpenBrain # NFS export path (for future use)
# Standalone MCP server for Claude.ai (OAuth 2.1 + GitHub identity).
enable_open_brain_mcp: true
Tip
The NFS export is used by the daily/weekly backup CronJobs in the backups
namespace — they dump supabase-db to NAS as compressed SQL files. See
Backup and Restore for how the CronJobs are scheduled and how to restore
from a dump. The live Supabase PostgreSQL database, Storage and MinIO all
run on static local-nvme PVs pinned to nuc2, which is more reliable for
database workloads than networked storage.
2 – Generate and seal credentials#
Generate all the secrets Supabase needs. You will need openssl, python3
with PyJWT, and kubeseal.
Generate raw credentials#
# JWT secret (signs all Supabase tokens)
JWT_SECRET=$(openssl rand -base64 32)
# Postgres password
DB_PASSWORD=$(openssl rand -base64 32)
# Realtime secret key base (Phoenix framework)
REALTIME_SECRET=$(openssl rand -base64 64)
# Meta crypto key (encrypts metadata at rest)
META_CRYPTO_KEY=$(openssl rand -hex 32)
# MCP access key (your x-brain-key for API auth)
MCP_ACCESS_KEY=$(openssl rand -hex 32)
# Dashboard password
DASHBOARD_PASSWORD=$(openssl rand -base64 24)
# Analytics key (required even though analytics is disabled)
ANALYTICS_KEY=$(openssl rand -base64 32)
Generate Supabase JWTs#
Install PyJWT if needed (uv pip install PyJWT), then generate the anon
and service_role tokens:
python3 -c "
import jwt, time
secret = '$JWT_SECRET'
print('ANON_KEY=' + jwt.encode(
{'role': 'anon', 'iss': 'supabase',
'iat': int(time.time()), 'exp': int(time.time()) + 10*365*24*3600},
secret, algorithm='HS256'))
print('SERVICE_KEY=' + jwt.encode(
{'role': 'service_role', 'iss': 'supabase',
'iat': int(time.time()), 'exp': int(time.time()) + 10*365*24*3600},
secret, algorithm='HS256'))
"
Save the output — you will need the ANON_KEY, SERVICE_KEY, and especially
the MCP_ACCESS_KEY later.
Create the namespace and seal the secret#
# Create namespace so kubeseal can scope the encryption
kubectl create namespace supabase
# Create and seal all credentials
kubectl create secret generic supabase-credentials \
--namespace=supabase \
--from-literal=secret="$JWT_SECRET" \
--from-literal=anonKey="$ANON_KEY" \
--from-literal=serviceKey="$SERVICE_KEY" \
--from-literal=password="$DB_PASSWORD" \
--from-literal=database=postgres \
--from-literal=username=admin \
--from-literal=dashboard-password="$DASHBOARD_PASSWORD" \
--from-literal=smtp-username=noreply@example.com \
--from-literal=smtp-password=dummy-not-configured \
--from-literal=secretKeyBase="$REALTIME_SECRET" \
--from-literal=cryptoKey="$META_CRYPTO_KEY" \
--from-literal=mcp-access-key="$MCP_ACCESS_KEY" \
--from-literal=publicAccessToken="$ANALYTICS_KEY" \
--from-literal=privateAccessToken="$ANALYTICS_KEY" \
--from-literal=openAiApiKey=not-configured \
--dry-run=client -o yaml | \
kubeseal --format yaml \
--controller-name sealed-secrets \
--controller-namespace kube-system \
> kubernetes-services/additions/supabase/templates/supabase-secret.yaml
Important
Save your MCP_ACCESS_KEY somewhere secure — you will need it to configure
the Claude.ai MCP connector. The sealed secret file is safe to commit to Git.
3 – Commit and deploy#
git add kubernetes-services/values.yaml \
kubernetes-services/additions/supabase/templates/supabase-secret.yaml
git commit -m "Enable Open Brain (Supabase) with sealed credentials"
git push
Then deploy via the playbook:
ansible-playbook pb_all.yml --tags cluster
ArgoCD will create the supabase Application and deploy all components.
Monitor progress:
# Watch ArgoCD app status
kubectl get app supabase -n argo-cd -w
# Watch pods come up
kubectl get pods -n supabase -w
All 10 pods should reach Running status within a few minutes:
db, auth, rest, realtime, storage, functions, studio, kong, meta, minio.
4 – Verify the deployment#
Check the database schema#
# Verify pgvector extension
kubectl exec -n supabase supabase-supabase-db-0 -- \
psql -U supabase_admin -d postgres \
-c "SELECT extname FROM pg_extension WHERE extname='vector';"
# Verify thoughts table
kubectl exec -n supabase supabase-supabase-db-0 -- \
psql -U supabase_admin -d postgres -c "\dt public.*"
Test the PostgREST API#
# Insert a test thought (use your SERVICE_KEY)
curl -s -X POST \
-H "apikey: $SERVICE_KEY" \
-H "Authorization: Bearer $SERVICE_KEY" \
-H "Content-Type: application/json" \
-H "Prefer: return=representation" \
-d '{"content":"Test thought","metadata":{"type":"test"}}' \
http://$(kubectl get svc -n supabase supabase-supabase-kong \
-o jsonpath='{.spec.clusterIP}'):8000/rest/v1/thoughts
# Retrieve it
curl -s \
-H "apikey: $SERVICE_KEY" \
-H "Authorization: Bearer $SERVICE_KEY" \
http://$(kubectl get svc -n supabase supabase-supabase-kong \
-o jsonpath='{.spec.clusterIP}'):8000/rest/v1/thoughts
5 – Configure Cloudflare Tunnel#
In the Cloudflare Zero Trust dashboard, add three public hostnames for your tunnel:
Hostname |
Service |
|---|---|
|
|
|
|
|
|
Note
The first two hostnames point to Kong (Supabase API gateway). The third
routes to the standalone MCP server via ingress-nginx. Configure a
Cloudflare Access bypass for brain.<your-domain> so the OAuth flow
is not blocked by browser-based Cloudflare authentication.
6 – Access Supabase Studio#
Supabase Studio is the admin UI for browsing tables, running SQL, and managing the database schema.
Via ingress (after Cloudflare tunnel setup): https://supabase.<your-domain>
Via port-forward (works immediately):
kubectl port-forward svc/supabase-supabase-kong -n supabase 8000:8000
# Open http://localhost:8000 in your browser
Login with the dashboard credentials you generated in step 2
(default username: admin).
7 – Connect Claude.ai and other clients#
Open Brain exposes three interfaces:
MCP server (
brain.<your-domain>/mcp) — for Claude.ai and other MCP-compatible clients. Uses OAuth 2.1 with GitHub identity. See Connect Claude.ai to Cluster Services via MCP for the full setup guide.REST API (
supabase-api.<your-domain>/functions/v1/open-brain-mcp) — for scripts, CLI tools, and direct API access. Usesx-brain-keyheader auth.Local CLI MCP server (
open-brain-cli/) — stdio-based MCP server for Claude Code with 6 tools including file upload/download. See section 8 below.
See Connect Claude.ai to Cluster Services via MCP for connecting Claude.ai via the MCP server, including GitHub OAuth App setup, project instructions, and troubleshooting.
8 – Connect Claude Code (local MCP server)#
The local MCP server gives Claude Code direct access to Open Brain via stdio, with file upload/download that bypasses the MCP context window.
Quick setup#
If you have uv and claude installed:
curl -fsSL https://raw.githubusercontent.com/gilesknap/tpi-k3s-ansible/main/scripts/setup-brain-cli | bash
The script prompts for three credentials:
BRAIN_API_URL — your Supabase API URL (e.g.
https://supabase-api.example.com)BRAIN_API_KEY — the
x-brain-keyshared secret (theMCP_ACCESS_KEYfrom step 2)BRAIN_SERVICE_KEY — the Supabase
SERVICE_KEYJWT (from step 2)
It installs the open-brain-cli package and adds an entry to
the MCP server via claude mcp add. Restart Claude Code and verify with /mcp.
Manual setup#
If you prefer to configure manually:
# Clone and install
git clone https://github.com/gilesknap/tpi-k3s-ansible.git
cd tpi-k3s-ansible/open-brain-cli
uv sync
# Register with Claude Code
claude mcp add open-brain \
-e "BRAIN_API_URL=https://supabase-api.example.com" \
-e "BRAIN_API_KEY=your-mcp-access-key" \
-e "BRAIN_SERVICE_KEY=your-service-role-jwt" \
-- uv run --project /path/to/open-brain-cli open-brain-cli
Available tools#
The local MCP server provides six tools:
Tool |
Description |
|---|---|
|
Save text with structured metadata |
|
Search by keyword and metadata filters |
|
List recent thoughts with filters |
|
Aggregate statistics |
|
Upload a local file to a thought |
|
Download an attachment to |
File uploads go directly to the Supabase Storage API over HTTP — no base64 through the context window.
File Attachments (Images, PDFs)#
Open Brain supports saving file attachments alongside thoughts using MinIO
as an S3-compatible object store. Files are stored in a private Supabase
Storage bucket (brain-attachments) backed by a local-nvme PVC on nuc2.
How it works#
File uploads go directly to the Supabase Storage API, never through the MCP context window. The local CLI (
upload_attachment) and future Slack bot both use this path.Text capture via the public MCP server (
capture_thought) is text-only. Claude.ai summarises binary content as text before saving.File retrieval via the public MCP server (
get_attachment) downloads from MinIO and returns base64 — works for viewing individual files.
Enable MinIO#
MinIO is enabled by default in the Supabase Helm values. If you deployed before
this feature was added, verify that deployment.minio.enabled: true is set in
kubernetes-services/templates/supabase.yaml. After pushing the change, ArgoCD
will deploy MinIO automatically.
The MinIO pod needs its own local-nvme PVC on nuc2. Verify it is
provisioned and bound:
kubectl get pvc -n supabase | grep minio
Add the Supabase service key to the MCP secret#
The MCP server needs the Supabase service-role key for get_attachment. See
Connect Claude.ai to Cluster Services via MCP step 3 for the kubectl/kubeseal pipeline that re-seals
open-brain-mcp-secret with the database password, service key, GitHub OAuth
credentials, and a fresh JWT signing secret. Commit and push the updated
sealed secret.
Disable Open Brain#
To remove Supabase from your cluster, set enable_supabase: false in
kubernetes-services/values.yaml, commit, push, and re-run the playbook.
ArgoCD will prune the Supabase Application resources, but the underlying
data directories on nuc2 (/home/k8s-data/supabase-db,
/home/k8s-data/supabase-storage, /home/k8s-data/supabase-minio) are
preserved across rebuilds and are only removed if you explicitly pass
-e wipe_local_data=true to pb_decommission.yml.
Troubleshooting#
Kong OOMKilled#
Kong requires at least 2Gi memory limit. If it keeps restarting with
OOMKilled, check the resource limits in templates/supabase.yaml.
“No API key found in request”#
Supabase Kong uses the apikey header (not Authorization alone).
Always include both headers:
-H "apikey: $KEY" -H "Authorization: Bearer $KEY"
Database pod CrashLoopBackOff#
If the DB pod fails with chown: Operation not permitted, the storage
backend does not support ownership changes. The default configuration uses
a static local-nvme PV on nuc2 (backed by a plain hostPath under
/home/k8s-data/supabase-db), which handles ownership changes correctly.
If you switched to NFS, you need no_root_squash on the NFS export.
ArgoCD “not permitted in project”#
The Supabase Helm chart repo must be in the ArgoCD project’s sourceRepos.
Check argo-cd/argo-project.yaml includes:
- "https://supabase-community.github.io/supabase-kubernetes"