Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ QDRANT_HTTPS=true
QDRANT_API_KEY=replace-me

# mem0 core
MEM0_COLLECTION=ian_memories
MEM0_DEFAULT_USER_ID=ian
MEM0_COLLECTION=memories
MEM0_DEFAULT_USER_ID=default-user

# LLM for fact extraction
MEM0_LLM_PROVIDER=anthropic
Expand Down
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Dependencies are pinned in `requirements.txt` (per PRD Appendix B); CI is in `.g

## Deployment notes

- Deploy is **push-to-`main` → CapRover webhook**, independent of CI status. The main app is stateless (Phase 1); only Phase 2 OAuth uses the `/app/data` persistent volume.
- Two supported deploy paths, same app image: **CapRover** (connects to an external Qdrant) and **Docker Compose** (`docker-compose.yml`, bundles Qdrant + app for non-CapRover hosts). The compose file overrides `QDRANT_HOST`/`QDRANT_PORT`/`QDRANT_HTTPS` to point at the in-stack `qdrant` service — keep that override if you edit it. Document changes to either path in `docs/USER_GUIDE.md`.
- Deploy (CapRover) is **push-to-`main` → CapRover webhook**, independent of CI status. The main app is stateless (Phase 1); only Phase 2 OAuth uses the `/app/data` persistent volume.
- `/healthz` does a real 2s-timeout round-trip to Qdrant and returns 503 if unreachable — keep the timeout so CapRover health checks don't hang.
- The `mem0-backup` app lives in this same repo as a second CapRover app (separate `captain-definition` / relative path). It snapshots Qdrant, uploads to S3, prunes by retention.
42 changes: 33 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
A self-hosted [mem0](https://github.com/mem0ai/mem0) memory server that exposes one shared
memory store over **two protocols from a single process**:

- **REST** (`/api/v1/memories…`) for scripts, n8n, curl, and the Hermes Agent.
- **REST** (`/api/v1/memories…`) for scripts, n8n, curl, and custom agents.
- **Streamable HTTP MCP** (`/mcp/`) for Claude Code, Claude Desktop, Claude.ai web, and Cowork.

It uses an existing external **Qdrant** instance as the vector backend, deploys to **CapRover**
on push to `main`, and ships a companion `mem0-backup` app that snapshots Qdrant to S3 nightly.
It uses **Qdrant** as the vector backend and runs two ways: a **Docker Compose** stack that bundles
Qdrant and the app together, or on **CapRover** (auto-deploys on push to `main`) against an existing
external Qdrant, with a companion `mem0-backup` app that snapshots Qdrant to S3 nightly.
This is a **single-user** system: `MEM0_DEFAULT_USER_ID` is the only user.

**Documentation:**
Expand All @@ -20,7 +21,7 @@ The sections below are a quick reference; the guides above go deeper.
## Architecture

```
Clients (Claude Code / Desktop / Hermes / curl) ──Bearer──┐
Clients (Claude Code / Desktop / agents / curl) ──Bearer──┐
Anthropic cloud (Claude.ai / Cowork) ──OAuth 2.1 + PKCE──┐ │
▼ ▼
CapRover app: mem0-server (this repo)
Expand Down Expand Up @@ -61,12 +62,30 @@ the full list. Key ones:
| Variable | Notes |
|---|---|
| `QDRANT_HOST`, `QDRANT_API_KEY` | external Qdrant instance |
| `MEM0_DEFAULT_USER_ID` | the single user (e.g. `ian`) |
| `MEM0_DEFAULT_USER_ID` | the single user (e.g. `default-user`) |
| `MEM0_EMBED_DIMS` | **must** match the embedder's output dim (3-small=1536) |
| `MEM0_API_KEY` | static bearer token; `openssl rand -hex 32` |
| `PUBLIC_BASE_URL` | public URL, used in OAuth metadata |
| `OAUTH_SIGNING_KEY` | PEM RSA private key; setting it enables Phase 2 OAuth |

## Deploy with Docker Compose

The simplest way to self-host if you don't already run CapRover. The bundled
`docker-compose.yml` brings up **Qdrant and the app together** — no external
Qdrant required.

```bash
cp .env.example .env # fill in ANTHROPIC_API_KEY, OPENAI_API_KEY, MEM0_API_KEY, QDRANT_API_KEY
docker compose up -d
```

The compose file points the app at the in-stack Qdrant automatically (you don't
need to touch `QDRANT_HOST`/`QDRANT_PORT`/`QDRANT_HTTPS`). The server comes up at
`http://localhost:8000` — REST under `/api/v1`, MCP at `/mcp`. For Phase 2 OAuth,
set `OAUTH_SIGNING_KEY` and a public `PUBLIC_BASE_URL` in `.env` and put the stack
behind an HTTPS reverse proxy. See the
[User Guide](docs/USER_GUIDE.md#deploying-with-docker-compose) for details.

## Production deploy (CapRover)

1. Create the `mem0-server` app. Enable **Has Persistent Data** and map `/app/data`
Expand Down Expand Up @@ -96,17 +115,22 @@ claude mcp add --scope user --transport http mem0-remote \
```bash
curl -X POST https://mem0.your-domain.com/api/v1/memories \
-H "Authorization: Bearer $MEM0_API_KEY" -H "Content-Type: application/json" \
-d '{"content": "Ian uses CapRover on DO", "agent_id": "n8n-flow"}'
-d '{"content": "We deploy services with CapRover", "agent_id": "n8n-flow"}'

curl -X POST https://mem0.your-domain.com/api/v1/memories/search \
-H "Authorization: Bearer $MEM0_API_KEY" -H "Content-Type: application/json" \
-d '{"query": "where does Ian host things?"}'
-d '{"query": "how do we deploy services?"}'
```

**Claude.ai web / Cowork (Phase 2):** add a custom connector pointing at
`https://mem0.your-domain.com/mcp/`, leave client ID/secret blank (DCR registers
automatically), and complete the consent redirect.

**Make agents actually use it:** connecting a client only makes the memory tools available — add a
short instruction block to your `CLAUDE.md` / ChatGPT custom instructions / `AGENTS.md` so the
agent recalls and saves memory every session. Copy-paste snippets are in the
[User Guide](docs/USER_GUIDE.md#prompting-agents-to-use-memory).

A smoke test against a live server is in `scripts/smoke.sh`.

## Restore drill
Expand All @@ -118,11 +142,11 @@ aws s3 cp s3://<bucket>/mem0-backups/2026-05-20T03-00-00Z.snapshot ./
# 2. Upload to Qdrant
curl -X POST -H "api-key: $QDRANT_API_KEY" \
-F "snapshot=@2026-05-20T03-00-00Z.snapshot" \
"https://qdrant.your-domain.com/collections/ian_memories/snapshots/upload"
"https://qdrant.your-domain.com/collections/memories/snapshots/upload"

# 3. Verify
curl -H "api-key: $QDRANT_API_KEY" \
"https://qdrant.your-domain.com/collections/ian_memories"
"https://qdrant.your-domain.com/collections/memories"
```

## Troubleshooting
Expand Down
12 changes: 10 additions & 2 deletions app/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,18 @@ def __init__(
static_token: str,
jwt_public_key: str | None = None,
issuer: str | None = None,
static_client_id: str = "default-user",
):
super().__init__()
self.static_token = static_token
self.jwt_public_key = jwt_public_key
self.issuer = issuer
self.static_client_id = static_client_id

async def verify_token(self, token: str) -> AccessToken | None:
if self.static_token and secrets.compare_digest(token, self.static_token):
return AccessToken(
token=token, client_id="ian", scopes=["read", "write"]
token=token, client_id=self.static_client_id, scopes=["read", "write"]
)
if not self.jwt_public_key:
return None
Expand Down Expand Up @@ -89,6 +91,7 @@ def build_verifier() -> AuthProvider:
static_token=s.mem0_api_key,
jwt_public_key=public_key_pem(),
issuer=base,
static_client_id=s.mem0_default_user_id,
)
# Wrap the verifier so the mounted MCP app advertises the protected
# resource metadata URL in the 401 WWW-Authenticate header (RFC 9728).
Expand All @@ -105,5 +108,10 @@ def build_verifier() -> AuthProvider:
scopes_supported=SCOPES,
)
return StaticTokenVerifier(
tokens={s.mem0_api_key: {"client_id": "ian", "scopes": ["read", "write"]}}
tokens={
s.mem0_api_key: {
"client_id": s.mem0_default_user_id,
"scopes": ["read", "write"],
}
}
)
2 changes: 1 addition & 1 deletion app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class Settings(BaseSettings):
qdrant_api_key: str

# mem0 core
mem0_collection: str = "ian_memories"
mem0_collection: str = "memories"
mem0_default_user_id: str

# LLM (fact extraction)
Expand Down
2 changes: 1 addition & 1 deletion app/oauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ def _issue_tokens(client_id: str) -> dict:
now = int(time.time())
claims = {
"iss": s.public_base_url.rstrip("/"),
"sub": "ian",
"sub": s.mem0_default_user_id,
"aud": "mem0-server",
"scope": " ".join(SCOPES),
"client_id": client_id,
Expand Down
46 changes: 46 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Self-contained mem0-server stack: Qdrant + the app in one Docker Compose project (two containers).
# For users who don't run CapRover. See docs/USER_GUIDE.md → "Deploying with Docker Compose".
#
# cp .env.example .env # then fill in the API keys
# docker compose up -d
#
# The app is reachable at http://localhost:8000 (REST under /api/v1, MCP at /mcp).

services:
qdrant:
# Pinned for reproducible builds; aligned with the qdrant-client version in
# requirements.txt. Bump both together when upgrading Qdrant.
image: qdrant/qdrant:v1.18.0
restart: unless-stopped
environment:
# Qdrant enforces this key when set; the app sends it via QDRANT_API_KEY.
QDRANT__SERVICE__API_KEY: ${QDRANT_API_KEY:?set QDRANT_API_KEY in .env}
volumes:
- qdrant_data:/qdrant/storage
# Uncomment to inspect Qdrant from the host (dashboard at :6333/dashboard).
# ports:
# - "6333:6333"

mem0-server:
build: .
restart: unless-stopped
depends_on:
- qdrant
env_file:
- .env
# These override whatever is in .env so the app always talks to the
# in-stack Qdrant over the internal network, not an external host.
environment:
QDRANT_HOST: qdrant
QDRANT_PORT: "6333"
QDRANT_HTTPS: "false"
PUBLIC_BASE_URL: ${PUBLIC_BASE_URL:-http://localhost:8000}
ports:
- "8000:8000"
volumes:
# Persists the Phase 2 OAuth SQLite DB across restarts.
- mem0_data:/app/data

volumes:
qdrant_data:
mem0_data:
10 changes: 8 additions & 2 deletions docs/DEVELOPER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ scripts/ smoke.sh (REST) and smoke_mcp.py (MCP) against a live server
docs/ PRD.md (spec), USER_GUIDE.md, DEVELOPER_GUIDE.md.
Dockerfile Main app image; runs uvicorn with --workers 2.
captain-definition CapRover build descriptor for the main app.
docker-compose.yml Self-contained stack (Qdrant + app) for non-CapRover hosts.
```

## Request and data flow
Expand Down Expand Up @@ -225,8 +226,13 @@ reads the `Authorization` header, so tokens are never logged.

- **CI** (`.github/workflows/ci.yml`) runs on pushes to `main` and on all PRs: installs deps, then
`ruff check app/` and `pytest -q` on Python 3.12.
- **Deploy** is push-to-`main` → CapRover webhook, independent of CI status. The main app builds
from the root `Dockerfile` / `captain-definition` and runs `uvicorn app.main:app --workers 2`.
- **Deploy** has two supported paths (same app image, different infrastructure):
- **CapRover** — push-to-`main` → CapRover webhook, independent of CI status. The main app builds
from the root `Dockerfile` / `captain-definition` and runs `uvicorn app.main:app --workers 2`.
Connects to an external Qdrant.
- **Docker Compose** — `docker-compose.yml` builds the same `Dockerfile` and brings up the app
alongside a bundled Qdrant service, overriding `QDRANT_HOST`/`QDRANT_PORT`/`QDRANT_HTTPS` to the
in-stack service. See the [User Guide](USER_GUIDE.md#deploying-with-docker-compose).
- The **backup app** is a second CapRover app built from `backup/` (separate `captain-definition`).
See the [User Guide](USER_GUIDE.md#2-deploy-the-backup-app-mem0-backup).
- The main app is stateless in Phase 1; only Phase 2 OAuth uses the `/app/data` persistent volume
Expand Down
Loading
Loading