FishMem

Self-hosted dashboard

Run the FishMem control-plane — dashboard, API keys, operations, webhooks, REST API, and file extraction — with Docker or on Cloudflare.

The fishmem engine is a library you embed. This page is about the layer above it: the control-plane, the full application that wraps the engine with a UI and a REST surface. It ships as the apps/web app in the open-source fishmem-labs/fishmem repo, and is the same app that powers FishMem Cloud.

If you only need memory inside one service, self-host the engine instead — see Self-hosting the engine. Run the control-plane when you want a team-facing platform.

What it provides

  • A web dashboard to manage projects, keys, and memories.
  • A Sources workspace for direct text, asynchronous PDF/Office/image extraction, immutable originals, and citation-ready RAG.
  • API key management — issue and revoke fm_... keys per project.
  • A usage view over operations and storage.
  • Webhooks that fire on memory events.
  • The FishMem REST API — the same add / search / get / update / delete / history surface documented in the API reference.

Choose one runtime:

RuntimeDurable dataFile extraction
Node/DockerlibSQL plus a persistent asset volumePoll worker plus pinned Docling container
CloudflareD1, Vectorize, and R2Queue plus pinned Docling Container

Both use the same document-ingestion module, task states, retry rules, and artifact format. The open-source control plane leaves out managed billing, operated infrastructure, and SLA.

Run with Docker

The bundled Compose stack is the shortest complete self-host path.

git clone https://github.com/fishmem-labs/fishmem
cd fishmem
cp .env.example .env

Set OPENAI_API_KEY, BETTER_AUTH_SECRET, FISHMEM_SETUP_TOKEN, and CRON_SECRET to distinct non-placeholder values in the root .env. Compose fails closed when the three application secrets are absent. Then start the web process and durable task poller:

docker compose up -d web task-worker
docker compose logs -f web task-worker extractor

Compose brings up:

  • web on http://localhost:3000;
  • extractor, the pinned CPU Docling image on the private Compose network;
  • task-worker, which wakes due tasks every two seconds and maintenance daily;
  • one persistent fishmem-webdata volume containing libSQL and raw/artifact objects.

The web container applies schema changes before startup. Back up the whole volume: the relational rows and object directory form one retention unit. Uploads left incomplete for 24 hours are removed by maintenance.

Run on Cloudflare

Prerequisites

  • A Cloudflare account with Workers enabled.
  • A D1 database, a Vectorize index, an R2 bucket, two Queues, and Containers enabled in that account.
  • An OpenAI-compatible LLM provider and embedding provider. The shipped defaults are gpt-4o-mini and text-embedding-3-small, but base URLs and models are configurable.
  • Node.js and the Wrangler CLI installed locally.

Clone the repo

git clone https://github.com/fishmem-labs/fishmem
cd fishmem
pnpm install
cd apps/web

Configure env and secrets

Bindings live in wrangler.jsonc; secrets are set with wrangler secret put. The required variables:

VariableTypeDescription
OPENAI_API_KEYsecretLLM + embedding provider key.
BETTER_AUTH_SECRETsecretSigning secret for dashboard sessions.
FISHMEM_SETUP_TOKENsecretOne-time proof required to create the first production admin. Use a different random value from the session secret.
CRON_SECRETsecretAuthenticates scheduled task/maintenance calls.
D1D1 bindingThe graph and control-plane database.
VECTORIZEVectorize bindingThe vector index.
R2R2 bindingObject storage bucket.
DOCUMENT_TASKSQueue bindingLow-latency extraction wakeups.
DOCUMENT_EXTRACTORDurable Object bindingRoutes work to the Docling Container.
MEMORY_RATE_LIMITERRate Limit bindingPer-key public API protection.
FISHMEM_TRUSTED_IP_HEADERSoptional variableComma-separated client-IP headers trusted by auth. Cloudflare defaults to cf-connecting-ip; a Node reverse proxy must overwrite its configured header.

Set the secrets:

wrangler secret put OPENAI_API_KEY
wrangler secret put BETTER_AUTH_SECRET
wrangler secret put FISHMEM_SETUP_TOKEN
wrangler secret put CRON_SECRET

The repository already declares these bindings in apps/web/wrangler.jsonc. Replace the resource names or IDs for your account. Create the queue resources before deployment:

wrangler queues create fishmem-document-tasks
wrangler queues create fishmem-document-tasks-dlq
wrangler r2 bucket create fishmem

The relevant binding shape is:

wrangler.jsonc
{
  "d1_databases": [{ "binding": "D1", "database_name": "fishmem", "database_id": "<id>" }],
  "vectorize": [{ "binding": "VECTORIZE", "index_name": "fishmem-memories" }],
  "r2_buckets": [{ "binding": "R2", "bucket_name": "fishmem" }],
  "containers": [{
    "class_name": "FishMemExtractorContainer",
    "image": "./services/extractor/Dockerfile",
    "max_instances": 3,
    "instance_type": "standard-2"
  }],
  "durable_objects": {
    "bindings": [{
      "name": "DOCUMENT_EXTRACTOR",
      "class_name": "FishMemExtractorContainer"
    }]
  },
  "queues": {
    "producers": [{
      "binding": "DOCUMENT_TASKS",
      "queue": "fishmem-document-tasks"
    }]
  },
  "ratelimits": [{
    "name": "MEMORY_RATE_LIMITER",
    "namespace_id": "1001",
    "simple": { "limit": 600, "period": 60 }
  }]
}

Create Vectorize metadata indexes

Create every filter index before accepting the first write. Cloudflare Vectorize applies a metadata index only to vectors inserted after that index exists.

wrangler vectorize create fishmem-memories --dimensions=1536 --metric=cosine
for prop in namespaceId recordKind userId agentId runId memoryType documentId sourceKey; do
  wrangler vectorize create-metadata-index fishmem-memories \
    --property-name="$prop" --type=string
done

namespaceId is the project fence. recordKind prevents document chunks and memories from entering each other's retrieval lane; documentId and sourceKey enable source-scoped RAG. The eight properties remain within Vectorize's ten metadata-index limit. FishMem still rehydrates each candidate from D1 and reapplies the full scope; the index is not the final authorization decision.

Run migrations

Apply the schema to your D1 database:

pnpm run deploy:database

FishMem writes an immutable source version, its deterministic chunks, and the current-source pointer in one transactional D1 batch. Chunk rows are expanded from bounded JSON payloads, so a large textual source does not spend one D1 subquery per chunk.

Direct textual originals and asynchronous file assets/artifacts are written to R2. Reads verify SHA-256 and byte length; document deletion and project purge remove the linked object family. The Sources screen uses the public three-step upload lifecycle and exposes durable extraction status; browser-side file selection is not a second source writer.

Deploy to Workers

pnpm run deploy

Wrangler prints the deployed URL. The dashboard is served at the root; the FishMem API is under /v1.

Create your first project and API key

Open the deployed URL. On /setup, create the first admin and enter the deploy-time FISHMEM_SETUP_TOKEN. Production rejects direct first-user signup without this proof, so a newly deployed public Worker cannot be claimed by its first visitor. The token is not stored in FishMem's database and is ignored after the initial account exists.

Then sign in, create a project, and issue an API key from the dashboard. Call your own instance exactly like the hosted cloud:

curl https://<your-worker>.workers.dev/v1/memories \
  -H "Authorization: Bearer fm_..." \
  -H "Content-Type: application/json" \
  -d '{
    "messages": [{"role": "user", "content": "I prefer dark mode."}],
    "user_id": "alex"
  }'

The Worker dispatches export/import, rebuild, derivation, and maintenance tasks. File completion sends a Queue wakeup; the Queue consumer processes one extraction task per message with bounded concurrency. A */1 * * * * trigger repairs lost wakeups and expired leases, while 0 4 * * * also enqueues nightly maintenance. Keep both cron entries and CRON_SECRET configured.

The container image is pinned in apps/web/services/extractor/Dockerfile. Extraction uses Docling's async submit/status/result endpoints. The D1 task row—not the Queue or Container instance—owns retries and terminal status.

Self-hosting and FishMem Cloud share the same memory engine and public memory contract. Cloud additionally operates billing, scaling, updates, and hosted organization features. Self-hosting means you operate resources, apply migrations on upgrade, and monitor usage yourself.

Next steps

On this page