Skip to main content

Hosted Server (Beta)

This is early and moving fast. The hosted server’s capture, recall, metering, and deletion paths described below are real and tested, but the UX and developer experience around them are still being built — there’s no polished dashboard, onboarding flow, or self-serve signup yet. Expect the plumbing to be solid and the paving to be unfinished. Routes, env var names, and the first-key bootstrap flow may shift as we wire up the dashboard. Pin a version if you’re integrating.
The hosted server is the cloud side of claude-mem: a Postgres-backed HTTP service (/v1) plus a separate BullMQ generation worker. Where the local plugin keeps memory in ~/.claude-mem/claude-mem.db on your machine, the hosted server keeps it per team and per project in Postgres, and exposes it back to any MCP client over an authenticated link. Three capabilities landed together and are documented here:

Remote MCP recall

Paste an authenticated link into Claude Code to recall your cloud memory — read-only, team/project-scoped.

Paid-readiness

Opt-in rate limiting, monthly request/token quotas, and usage metering — the guards a paid tier needs.

Data deletion

Right-to-erasure: forget a single memory, or purge everything captured for a project.

The shape of the system

Every row is scoped by (team_id, project_id). An API key carries a team (always) and an optional project scope; that scoping bounds every read, write, and delete.

Authentication

Set CLAUDE_MEM_AUTH_MODE=api-key and send Authorization: Bearer <key> on every request. Scopes gate access:
  • Read endpoints (search, context, recall, usage) require memories:read.
  • Write endpoints (ingest, key issuance, deletion) require memories:write.
Keys are stored as SHA-256 hashes in the api_keys table; the raw cm_... value is shown exactly once, at mint time.

Remote authenticated MCP recall

/v1/mcp is a streamable-HTTP MCP server. It’s the secure link a user pastes into Claude Code to recall their cloud memory. It is read-only and authenticated by the same API key as the REST routes (memories:read); the key’s team — and project, if the key is project-scoped — bounds every read.
Smart search is the default three-step workflow. Review the index, inspect timeline context, then fetch only the selected records. There are no auto or guided modes. Each tool uses the authenticated team and requested project:
The transport is stateless — one MCP server + transport per request — so it needs no session affinity behind a load balancer. Mutating tools are intentionally absent: a pasted recall link can never write or delete. Every read is written to audit_log as an observation.read event, the same as POST /v1/search.

Connecting a client: key issuance + connect

Two routes turn “I have a server” into “Claude Code is recalling my cloud memory”:
  • POST /v1/keys (requires memories:write) mints a read-only API key for the caller’s team and returns a paste-ready connect command. The raw key appears once. Body: { "expiresInDays"?: number }. Minting requires write scope so a read-only key can’t escalate itself into more keys.
  • GET /v1/connect (requires memories:read) returns the same command with a <YOUR_API_KEY> placeholder — a GET never mints. The mcpUrl is built from CLAUDE_MEM_PUBLIC_URL (recommended when behind a proxy or load balancer) or, failing that, the request host.
First-key bootstrap is the rough edge. Minting a team’s very first key still needs a session-gated path (a web dashboard), because POST /v1/keys itself requires a write-scoped key. better-auth’s apiKey() plugin exists but writes to a different store than the Postgres api_keys these routes authenticate against — wiring the better-auth org → team mapping is the remaining piece, and the biggest part of the devex work still ahead.
These guards run after auth and are opt-in via environment variables. Unset (the default) means no rate limit, no quota, and no metering — behavior is identical to a server without them. Every guard fails open: a backing-store error never blocks a legitimate request. Token and observation metering is written to the same usage_events table from the generation worker, so usage reflects real provider spend, not just HTTP calls. GET /v1/usage returns the caller team’s per-kind totals for the current month:
“Gates writes only” is deliberate: ingestion is what drives generation, which is what costs tokens. A team that blows its token budget can still read its existing memory — you never lock someone out of their own data over billing.

Data deletion (forget)

Right-to-erasure. Both routes require memories:write and are scoped to the caller’s team. Both write an audit_log entry.
  • DELETE /v1/memories/:id — delete a single observation; its observation_sources cascade. Returns 404 if no such observation exists for the team. Audited as observation.deleted.
  • DELETE /v1/projects/:projectId/memory — purge all captured content for a project in one transaction: observations, raw agent events, server sessions, and generation jobs. The project shell (config/membership) is kept so the team can keep using it. Returns per-table counts. Returns 404 if the project doesn’t belong to the team. Audited as project.memory_purged.
Deletion is team-scoped at the SQL layer, so a key can only ever erase its own team’s data — a cross-team or nonexistent projectId returns 404 rather than a misleading success.

Event generation semantics

Ingestion (POST /v1/events) accepts two query flags that control observation generation:
  • generate=false — write the event but do not enqueue a generation job.
  • wait=true — return the generationJob descriptor so callers can poll GET /v1/jobs/:id for completion.
Without wait=true, the response includes the new event row plus a best-effort generationJob field. With wait=true, that field is always populated (or null only when generation was explicitly disabled). The actual provider call happens in the separate BullMQ worker (claude-mem server worker start) — the HTTP path never blocks on a provider response.

Generation concurrency

By default the generation worker processes one job at a time in each of its two lanes: event (per-event observations) and summary (session summaries). If events arrive faster than one job at a time can process them, jobs still complete but the backlog grows, and observations land hours after the work they describe. Set CLAUDE_MEM_SERVER_GENERATION_CONCURRENCY=N on the generation worker to run up to N jobs in parallel per lane. Both lanes use the value, so one worker process can have up to 2N provider calls in flight. Size N against your provider’s rate limit. Values above 64 are clamped to 64, and invalid values are ignored with a warning. If a worker stops making progress, stale processing locks can be reclaimed for another attempt. Each attempt may persist observations or report a failure only while it still owns the Postgres processing lock. A response from a reclaimed attempt is discarded so it cannot complete or fail the replacement attempt. This ownership check also applies to session summaries and does not hold a database lock during the provider call. Recovery may still require another paid provider call; it does not guarantee exactly-once provider execution.

Claude provider through a gateway

With CLAUDE_MEM_SERVER_PROVIDER=claude, the generation worker calls api.anthropic.com. To send its calls through an Anthropic-compatible gateway (Cloudflare AI Gateway, LiteLLM, a corporate proxy) for per-call logs, cost and rate limits, set these on the generation worker: Claude Code’s own ANTHROPIC_BASE_URL and ANTHROPIC_CUSTOM_HEADERS are not read, so a server started from a shell that sets them for Claude Code doesn’t send its key to that gateway. A cmem.ai memory key (cm_pro_…) is refused on this provider, as the API key or inside a custom header; it only works on the cmem.ai gateway.

Custom generation provider (advanced)

The generation worker ships with claude, gemini and openrouter providers, and each one reads a single set of credentials from its environment. If your deployment needs something else, such as a different key per project, set CLAUDE_MEM_SERVER_PROVIDER=custom and point CLAUDE_MEM_CUSTOM_PROVIDER_MODULE at a JavaScript module. The module must export createProvider(helpers), which returns an object with a generate(context) method. helpers gives the module this server’s prompt builder, its Anthropic client (ClaudeObservationProvider, which accepts a baseUrl), its retryable error class, the resolved maxOutputTokens, and an apiVersion (currently 1) that changes whenever any of those contracts change.
The module runs inside the generation worker with everything that process holds: the database URL, every provider key and the Redis credentials. Treat it like any other code you deploy. The path is read only from the process environment, never from settings.json or an API route. It must be an absolute path, and the worker logs the path and the file’s sha256 when it loads the module.

Search by meaning

With pgvector 0.8 or later in its Postgres, search ranks by meaning as well as by keyword: /v1/search, /v1/context and the MCP search and context tools fuse a keyword ranking with the observations nearest the query’s vector, so a query finds a memory that shares none of its words. The server embeds queries and the generation worker embeds the observations it writes, in-process with the model CLAUDE_MEM_EMBEDDING_MODEL names. The Docker image includes the default one; build it with --build-arg EMBEDDING_REPO=<repo>, the model’s repository in src/services/vector/models.ts, to include another, which would otherwise download at its first use. Without pgvector, or while the model cannot load, search is keyword-only. GET /v1/info reports vectorSearch: whether it is available, the model, how many observations a search by meaning cannot find yet, and the last embedding error. Outside Docker, the model (about 90 MB) downloads from huggingface.co on first start. The server begins loading it at startup; until it is ready, searches are keyword-only and new memories are saved without vectors (a request waits at most two seconds for it). The background backfill embeds those memories once the model loads, or at the latest at the next start. npx claude-mem server start, restart and worker start install the embedder’s packages (about 300 MB, once) unless CLAUDE_MEM_SERVER_VECTOR_SEARCH=false. Vectors cost about 28 KB of Postgres storage per observation (about 1 GB per 40,000 observations), plus CPU time to embed each one.

Endpoint reference

All endpoints are mounted under /v1; legacy worker routes remain under /api.
/v1/search, /v1/context and the MCP search tool match keywords with Postgres full-text search (English stemming). How plain words match depends on the Postgres the server runs on:
  • With pg_textsearch (the Docker Compose image): plain words match any of the query’s terms, ranked by bm25, so rare terms count for more than common ones and a question such as when did we change the retry limit finds the memory about the retry limit first. Memories with none of the terms are left out.
  • Without it (any other Postgres): plain words must all appear, ranked by ts_rank, as before.
Web-search syntax means the same on both: "quoted phrase" matches the words in that order, -term excludes memories containing it, and or matches either side. A query that uses any of them is filtered exactly as before; bm25 only orders the matches.

Upgrading the Postgres image (Docker Compose)

docker-compose.yml builds its Postgres image locally from docker/postgres/Dockerfile as claude-mem-postgres:17: Postgres 17 on Debian (trixie) with the pgvector and pg_textsearch extensions from the PostgreSQL project’s apt repository. No prebuilt image is pulled from a third-party registry. The image is about 650 MB, against 424 MB for the postgres:17-alpine image earlier releases used. Moving from Alpine to Debian changes the C library under your existing data volume (musl to glibc), which changes how text sorts. Indexes that order text by a C-library collation must be rebuilt, or lookups through them can miss rows. The one-shot postgres-collation service does this before the server and worker start: it finds databases whose recorded collation version differs from the C library’s, REINDEXes only the indexes that order text by collation (vector, full-text and jsonb indexes are left alone), and records the new collation version. A second start finds nothing to do. New databases use Postgres’s builtin C.UTF-8 locale, so later C-library changes cannot affect them. To upgrade an existing stack:
  1. Back up first. With the old stack still running:
    A copy of the volume works too: docker compose down, then docker run --rm -v <project>_postgres-data:/from -v "$PWD":/to alpine tar czf /to/postgres-data.tgz -C /from .
  2. Build and start only Postgres on the new image:
  3. Dry run to list the indexes that would be rebuilt, without changing anything:
  4. Start the rest. docker compose up -d runs the rebuild, then the server and worker. The rebuild takes seconds for typical databases.
Rollback. Restore the backup from step 1; don’t point the old image at the upgraded volume. After the rebuild its text indexes are ordered for glibc, and once the server has created pgvector or pg_textsearch objects, postgres:17-alpine (which has neither extension) cannot use them. Check out the previous docker-compose.yml, then either put the volume copy back (docker compose down, docker volume rm <project>_postgres-data, recreate it and untar postgres-data.tgz into it) or start the old stack on a fresh volume and load the dump with psql -U "$POSTGRES_USER" -d postgres -f claude-mem-backup.sql.

What’s solid vs. what’s coming

Solid today: Postgres-backed multi-tenant storage, api-key auth with read/write scopes, the /v1/mcp recall link, opt-in rate limiting + quotas + metering, and audited data deletion. All covered by the Postgres-gated e2e suite.Still being built (UX / devex): a web dashboard for the first-key bootstrap and key management, self-serve onboarding, a billing/plan UI on top of the metering primitives, and a smoother “connect Claude Code to my cloud memory” flow than pasting a CLI command. These are the next focus — the primitives above are the foundation they’ll sit on.