Show HN: Anansi – open-source memory API for LLM apps Anansi, an open-source, self-hostable memory API for LLM applications, was released on GitHub under the MIT license, with a commercial enterprise layer. It provides two API calls, /v1/ingest and /v1/context, to store and retrieve structured organizational memory with temporal reasoning, and runs on Postgres and Redis. The project aims to give AI agents a durable understanding of how an organization works and how that changes over time, with a quickstart that takes about five minutes. A self-hostable memory engine for AI agents that can distinguish what was true from what the agent knew at the time. Anansi gives an AI system a durable understanding of how an organization actually works — and how that changed over time. You feed it the exhaust your company already produces: conversations, docs, tickets, meeting transcripts. Anansi turns that into structured memory your agent can read before it answers, and keeps every version of it. So you can ask not just "what is our escalation process?" but "what did we think it was in March, and when did it change?" — and get an answer with a citation. TL;DR:MIT-licensed self-hostable core, with a commercial enterprise/hosted layer. See License . Two API calls do the work: POST /v1/ingest remember this GET /v1/context what do you know that's relevant right now? ingest returns 202 immediately and does the expensive work on a queue, so it never sits in your response path. context returns a compact, already-synthesized profile you can paste straight into a system prompt — not a pile of chunks to rank yourself. Self-hosted, MIT licensed, runs on Postgres and Redis. About five minutes to a working instance, no signup. What that citation actually buys you — the entity graph carries two independent time axes, so you can ask what was true and what the system knew, separately: The entity graph and temporal query results the two-axis reasoning above are a Pro+ feature. Self-hosted installs default to the enterprise plan see Plan limits plan-limits-do-not-apply-to-self-hosted-installs and get it automatically; on the hosted service it requires a paid tier. No account and no API key from us. Everything below runs on your machine. Timings are measured, not aspirational: the API image builds from source in about 90 seconds on a warm Docker, and the embedding model is a 274 MB download. The five-minute figure assumes option A or B in step 2 — option C pulls a 7 GB image and takes considerably longer. git clone https://github.com/g-33-L/anansi.git cd anansi docker compose up -d That brings up PostgreSQL, Redis, and the API. Migrations run automatically on first boot. The API and docs serve at http://localhost:3000 . Anansi needs an embedding model, and Compose does not start one for you. Skip this step and ingest will appear to succeed — it returns 202 because embedding is asynchronous — while retrieval fails with 503 . Open /status http://localhost:3000/status ; it reports the embedding backend explicitly, so you can see the problem rather than infer it. Three ways to satisfy it, ordered by how long they actually take. Only the first two keep this quickstart inside five minutes. A. You already run Ollama on your host — ~274 MB, under a minute ollama pull nomic-embed-text Nothing else to configure: Compose already points the container at host.docker.internal:11434 . This is the fastest path and the one to prefer if you have Ollama installed. B. Hosted embeddings — instant, needs a free Nomic key printf 'DEPLOYMENT MODE=hybrid\nINFERENCE LOCATION=local\nEMBEDDING LOCATION=cloud\nNOMIC API KEY=your key here\n' .env docker compose up -d api All four lines are required. DEPLOYMENT MODE defaults to local , which forbids cloud providers outright — supplying NOMIC API KEY without switching to hybrid is a deliberate startup failure, not an oversight, so the container refuses to boot and tells you so. hybrid is what lets you mix local inference with cloud embeddings. Confirm it took effect — the startup log states the resolved mode: startup Deployment mode: hybrid inference=local, embedding=cloud, telemetry=allowed Note that this sends the text you ingest to Nomic. Use A or C if that matters. C. Ollama inside Compose — fully self-contained, but a ~7 GB image pull first This is the slow path. ollama/ollama:latest is about 7 GB because it ships GPU runtimes, and on a normal connection the pull alone takes ten minutes or more. Right choice if you want everything in Compose and nothing on your host — not the right choice if you are trying Anansi for the first time. echo "OLLAMA BASE URL=http://ollama:11434" .env docker compose --profile local-ai up -d pulls the 7 GB image docker compose exec ollama ollama pull nomic-embed-text 274 MB Appending is safe even if OLLAMA BASE URL is already set — Compose takes the last definition. If you set it after the API was already running, restart it with docker compose up -d api so it picks up the new address. Whichever you pick, nomic-embed-text 274 MB is all that ingest and retrieval need. The much larger chat model llama3.1:8b , ~4.7 GB is only used to synthesize the static and dynamic profiles — pull it later with ollama pull llama3.1:8b when you want those. Keys live in your own database — this does not contact any hosted service: docker compose exec api node dist/scripts/seed-dev-key.js you@example.com It prints a key beginning ans . Export it: export ANANSI API KEY=ans ... Each email address gets its own workspace , and memory never crosses between them. Re-running the command with the same email issues another key into the same workspace; running it with a different email gives you a key that cannot see anything you stored earlier. If your data seems to have vanished, check which email the key came from. curl -X POST http://localhost:3000/v1/ingest \ -H "Authorization: Bearer $ANANSI API KEY" \ -H "Content-Type: application/json" \ -d '{"userId":"user 123","content":"User is building a voice agent. Prefers TypeScript. Team of 4.","sourceType":"conversation"}' Returns 202 immediately — embedding happens in the background. curl -G http://localhost:3000/v1/context \ -H "Authorization: Bearer $ANANSI API KEY" \ --data-urlencode "userId=user 123" \ --data-urlencode "q=what is the user building?" relevant comes back populated: { "relevant": { "content": "User is building a voice agent. Prefers TypeScript. Team of 4.", "similarity": 0.4821 } , "static": , "dynamic": } Two things are worth knowing about that response: Embedding is asynchronous, so a query issued within a second of ingest can be answered by keyword search alone. Ask again and you will see a real cosine score. Search is hybrid, so you get an answer either way rather than an empty result. similarity: 0 means the embedding had not landed yet.until a chat model is available for synthesis step 2 . That is expected, not a failure. static and dynamic stay empty To prove the semantic half is genuinely working, ask something that shares no words with what you stored: curl -G http://localhost:3000/v1/context \ -H "Authorization: Bearer $ANANSI API KEY" \ --data-urlencode "userId=user 123" \ --data-urlencode "q=which coding language do they like?" That scores higher 0.5915 than the keyword-overlapping question, because nothing in it matches literally — only in meaning. Check /status http://localhost:3000/status first: it reports Postgres, Redis, the queue, and the embedding backend, and returns 503 when any of them is down. A 503 from /v1/context names the failing dependency and how to fix it directly in the response body.| Local no keys, no account | Optional / external | | |---|---|---| | API, Postgres, Redis, workers | ✅ started by docker compose up -d | | | Embedding + synthesis Ollama | ✅ option A/C above | Nomic hosted embeddings option B , Cerebras/GitHub Models for synthesis | | Everything in | SENTRY DSN is optional and off by defaultThe full local path — Docker + Ollama, no connectors configured — never sends ingested content off your machine. Once you add a cloud embedding/LLM provider or a connector, that surface's data leaves the box; see Security security for exactly what each DEPLOYMENT MODE allows. The Compose defaults are deliberately development-only and are sufficient to start a disposable local stack without creating .env . The cryptographic values baked into docker-compose.yml are published in this repository and therefore public — never use them outside local development. For anything persistent, copy .env.example /g-33-L/anansi/blob/main/.env.example , generate distinct values for ENCRYPTION KEY , API KEY HMAC SECRET , CSRF SIGNING KEY , and QUERY API KEY openssl rand -hex 32 each , and read . /g-33-L/anansi/blob/main/docs/enterprise/self-hosting.md docs/enterprise/self-hosting.md Never change — all stored connector tokens are encrypted with it. ENCRYPTION KEY after first installIf you run Ollama on your host rather than via the local-ai profile, the Compose default host.docker.internal already points at it. A .env written for host-run pnpm dev will contain localhost:11434 , which inside a container means the container itself — Compose interpolates that file, so the API silently cannot reach your host Ollama. The 503 body names the address it tried, which is how you spot this. Prefer running from source with pnpm dev ? See CONTRIBUTING.md /g-33-L/anansi/blob/main/CONTRIBUTING.md — note that pnpm test needs DATABASE URL and REDIS URL in your shell, and that the suite TRUNCATE s the local database, so do not point it at anything you care about.Using the TypeScript SDK packages/sdk /g-33-L/anansi/blob/main/packages/sdk : python import AnansiMemory from "anansi-memory"; const memory = new AnansiMemory { apiKey: process.env.ANANSI API KEY, baseUrl: "http://localhost:3000", // required when self-hosting — see below } ; await memory.ingest { userId: "user 123", content: "User is building a voice agent. Prefers TypeScript. Team of 4.", sourceType: "conversation", } ; const ctx = await memory.context { userId: "user 123", q: "what is the user building?" } ; const systemPrompt = You are a helpful assistant.\n\n${memory.formatForPrompt ctx } ; Self-hosters: set the base URL.Every client defaults to the hosted API at https://anansimemory.com packages/sdk/src/index.ts:173 , packages/sdk-python/anansi memory/client.py:77 . If you skip it, your calls go to the hosted service rather than your own instance, and your local key will not authenticate there. The option is baseUrl TypeScript , base url Python , and ANANSI BASE URL MCP . Also shipped: Python anansi-memory , MCP server anansi-mcp , Vercel AI SDK middleware anansi-ai-sdk , LangChain/LangGraph anansi-langchain , and framework-agnostic tool definitions anansi-tools . All are thin HTTP clients over the same /v1 API and contain no logic of their own. Eleven routes, all under /v1 , all in apps/api/src/routes/v1.ts /g-33-L/anansi/blob/main/apps/api/src/routes/v1.ts : | Route | What it does | |---|---| POST /v1/ingest | Store content. Returns 202 . | POST /v1/ingest/batch | Same, many at once. | GET /v1/context | Synthesized profile + relevant chunks. The main read. | POST /v1/search | Raw hybrid search when you want chunks, not a profile. | GET /v1/memories | Paginated raw chunks for a user. | GET /v1/entities | The entity graph, with asOf / asOfKnowledge . | GET /v1/ledger | Cited claims as of a point in time. | GET /v1/ledger/divergences | Where documented practice disagrees with observed practice. | GET /v1/ledger/timeline | When each answer was adopted and superseded. | DELETE /v1/memory | Delete memories cascades to the entity graph . | DELETE /v1/user | Hard-delete a user: chunks, profile, graph. | Full reference: docs/api/reference.md /g-33-L/anansi/blob/main/docs/api/reference.md . Three things are worth understanding before you commit to this. Every edge in the entity graph carries two independent time axes lib/db/schema.ts /g-33-L/anansi/blob/main/apps/api/src/lib/db/schema.ts : valid time valid from / valid until — when it was true in the world knowledge time recorded at / valid until recorded at — when the system learned it Most memory stores have one clock, or none, and overwrite on update. That silently rewrites history: if you learn in June that someone left in April, a single-axis store now claims you always knew. Anansi keeps both, so GET /v1/entities?asOf=…&asOfKnowledge=… reconstructs the graph as it was true and as it was believed, at any instant. This is the bi-temporal model, borrowed from accounting systems; the implementation is getEntitiesForUser in lib/ai/query-engine.ts /g-33-L/anansi/blob/main/apps/api/src/lib/ai/query-engine.ts . Alongside the graph, Anansi keeps an append-only ledger of attestations lib/db/attestations-repo.ts /g-33-L/anansi/blob/main/apps/api/src/lib/db/attestations-repo.ts : trust-tiered observed / candidate claims, each backed by a verbatim quote located in a specific source chunk. Nothing is auto-published — confidence defaults to 0 and status defaults to candidate . Answers are never overwritten, only superseded. GET /v1/ledger/divergences is the payoff: it surfaces where a documented answer wiki, runbook disagrees with observed reality chat, tickets , and when the practice changed. Rows, vectors, and BM25 all live in Postgres. Retrieval is pgvector cosine similarity fused with ts rank BM25 by reciprocal rank fusion — a single SQL query, transactional with everything else. There is no separate vector database to operate or keep in sync. Version 0.3.1 . Honest read, component by component. Solid. The ingest → embed → synthesize → retrieve loop, the bi-temporal entity graph, hybrid retrieval, the SDKs, the API-key auth and rate limiting. 40 test files under apps/api/src/test/ , roughly 417 assertions; temporal-query.test.ts is the executable spec for the bi-temporal semantics. This is the part that has been exercised. Works, less proven. The ledger endpoints are shipped and tested but young. The connectors Slack, Notion, Google Docs, Linear, transcript webhooks work but each has had limited real-world mileage. Synthesis quality with a local Ollama model has not been systematically validated — evaluate it against your own data before relying on generated profiles. Extraction quality is measured and the weaknesses are named in apps/api/scripts/eval/BENCHMARK.md /g-33-L/anansi/blob/main/apps/api/scripts/eval/BENCHMARK.md ; read it rather than taking a number from this page. Experimental. Executable skills / procedure extraction lib/ai/skill-extraction.ts , lib/skill/ — schema and extraction exist, there are no public routes. apps/graph-explorer is a demo UI over GET /v1/entities , useful but not a supported product surface. Not claimed. No SOC 2, ISO 27001, or HIPAA certification. No data-residency enforcement beyond choosing where you deploy. No published DPA. These exist. They are also newer and less exercised than the memory engine, so here is precisely what is and is not true. | Capability | Status | |---|---| OIDC SSO | Implemented. Authorize → callback → JIT provision → session, at GET /sso/:slug/login and /callback lib/enterprise/sso/oidc.ts . Not integration-tested against every major IdP. | SAML 2.0 SSO | Implemented via @node-saml/node-saml with wantAssertionsSigned and wantAuthnResponseSigned both enforced, no unsigned fallback path lib/enterprise/sso/saml.ts . Live at POST /sso/:slug/acs ; SP metadata at /sso/:slug/metadata . Unit tests cover config validation and profile mapping only — there is no end-to-end test against a real IdP. | SCIM 2.0 | Users and Groups, per-org bearer token, mounted at /scim/v2 lib/enterprise/scim/handler.ts . Users: list, get, create, PATCH/PUT active , delete = suspend membership, not global delete . Groups map to teams: list and create only — no group-membership sync, no group update or delete. Filters support userName eq and emails.value eq ; anything else returns the full list. | RBAC console | 6 roles owner , admin , member , billing , auditor , viewer over 28 permissions, single source of truth in lib/identity/roles.ts , enforced by requirePermission on every /console route. | API key scopes /v1 | Separate, coarser mechanism — five scopes ingest , read , entities , ledger , admin enforced on all 11 /v1 routes via requireScope routes/v1.ts:202 ; validateApiKey loads them in the existing auth query, so there is no extra round trip. A key with no scope rows is unrestricted — the console's documented back-compat rule, so no pre-existing key changes behaviour. Denial is 403 with code: "insufficient scope" , plus required scope and key scopes in the body. See "Authentication and key scopes" in docs/api/reference.md | Audit log | Append-only audit events , never updated or deleted, NDJSON export by keyset lib/enterprise/audit.ts . Writes are best-effort and swallowed on failure by design. Emitted from console API-key, member, SSO, and enterprise-admin actions — not from the . Ingest and retrieval are not audited. /v1 data plane | Approval workflow | Generic approval queue for skill publish , role grant , data export , connector add lib/enterprise/governance.ts . Only one action currently enforces an approval: audit export, which returns 403 without an approved data export request. The other kinds are recorded, not enforced. | Configurable PII redaction | Per-org rules named detectors or regex; mask / drop / hash applied after the built-in secret scrubber, wired into both the /v1/ingest path and the ingestion worker lib/enterprise/redaction.ts . | Signed licenses | ed25519-verified, organization-bound, fail-closed lib/enterprise/license.ts . See the note below. | How the gate works, plainly. Console enterprise routes sit behind requireEnterprise , which needs the org's edition to be enterprise plus — outside cloud mode — a license signed by the key in LICENSE PUBLIC KEY . Because you set LICENSE PUBLIC KEY yourself on a self-hosted install, you can generate an ed25519 keypair and mint your own license. The gate is a deployment control, not a lock. That is deliberate and we would rather say it than have you discover it. Verifiable in this repo, not claims: API keys HMAC-SHA256 hashed at rest; the raw key is shown once and never stored lib/auth/api-auth.ts . Connector tokens encrypted with AES-256-GCM under ENCRYPTION KEY lib/utils/crypto.ts . SSRF guards on every outbound fetch — URL ingestion and developer webhooks resolve DNS and reject private and loopback addresses, re-checking each redirect hop lib/infra/safe-fetch.ts . Rate limiting per workspace via a Redis sliding-window sorted set in a single Lua script, with monthly quota on top lib/infra/rate-limit.ts . Input safety — secret redaction and prompt-injection neutralization run on all end-user content before storage and before any LLM prompt lib/utils/sanitize.ts . Tenant isolation is application-layer. Every query scopes by workspace id / developer id . Postgres RLS on memory chunks is a NULL-guard backstop, not the boundary. If you are contributing, any new query must carry the scoping predicate. See docs/architecture/security-model.md /g-33-L/anansi/blob/main/docs/architecture/security-model.md . Disclosure policy: SECURITY.md /g-33-L/anansi/blob/main/SECURITY.md . DEPLOYMENT MODE controls whether content can leave the machine lib/config/deployment.ts : local — air-gapped. Inference and embeddings run on Ollama; content-exporting telemetry is off. The server refuses to start if a cloud LLM key, cloud embedding key, or Sentry DSN is set. This is enforced at boot, not documented and hoped for. hybrid — explicit per-capability mix via INFERENCE LOCATION and EMBEDDING LOCATION . cloud — the default; cloud providers when keys are present, local fallback otherwise. The engine is MIT. Ingestion, chunking, embedding, synthesis, the bi-temporal graph, the ledger, hybrid retrieval, the connectors, the SDKs, and basic multi-user identity organizations, members, API keys within a single self-hosted org. You can run all of it, forever, without talking to us. One layer is commercial, not MIT: enterprise auth SSO/SAML, SCIM provisioning , audit/governance/redaction workflows, team management, and the hosted control plane billing, the staff ops console . Those files carry a header naming LICENSE-EE — you can read and evaluate them freely, but running them in production requires a commercial license. See LICENSE-EE /g-33-L/anansi/blob/main/LICENSE-EE for the exact terms, and LICENSE for the full path list. This is the same shape as GitLab CE/EE or Sentry's open-core split: the code is visible, the enterprise surface is licensed separately. The hosted service adds operations, not capability: managed Postgres/Redis and upgrades, self-serve signup and billing, managed connector OAuth apps so you don't register your own Slack/Notion/Google apps , support with a response time, and an issued enterprise license for the EE surface above. Two things to know before you assume "MIT means unlimited": - The plan tiers exist in the engine, but they do not apply to you. lib/billing/plans.ts and feature-gate.ts are MIT and part of the engine, and routes/v1.ts calls gateFeature at ten sites — that machinery is here because the same code runs the hosted service. On a self-hosted install it is inert: a workspace with no subscription row defaults to enterprise — unlimited, nothing expires, every retrieval feature on.The default is chosen by whether upgrades are actually purchasable, which is detected by whether Stripe is configured resolveDefaultPlan in lib/billing/plans.ts . No Stripe, no metering. Set ANANSI DEFAULT PLAN if you genuinely want to meter your own install.This is worth stating plainly because it used to be the other way round: the default was free everywhere, which on your own hardware meant 1,000 ingests/month and a 7-day retention window that a background worker enforced by deleting your data . A memory engine that forgets after a week is not a product, and we fixed it rather than documenting it. - Enterprise console routes sit behind an edition check — self-hostable, but the code backing SSO/SCIM/audit/governance is LICENSE-EE , not MIT, so running it in production needs a license from us even if you mint your own signing key. We are not going to relicense the engine or move existing MIT-licensed features behind a paywall. That guarantee is about the code above the line, not the EE surface below it — the MIT grant on the engine you already have is the part that is actually binding, not this paragraph. All public routes are prefixed /v1 and every response carries an API-Version header. Within a major version there are no breaking changes. Breaking changes ship only under a new major path e.g. /v2 , with 90 days' notice on the prior version. Issues and PRs welcome — CONTRIBUTING.md /g-33-L/anansi/blob/main/CONTRIBUTING.md has the setup, and is the map. Good first reading: /g-33-L/anansi/blob/main/ARCHITECTURE.md ARCHITECTURE.md routes/v1.ts the whole API in one file , then lib/ai/query-engine.ts , then apps/api/src/test/temporal-query.test.ts .Open-core. MIT for the engine, LICENSE-EE for the enterprise surface described above — see Open core: what's here, what isn't open-core-whats-here-what-isnt for the full path list, LICENSE /g-33-L/anansi/blob/main/LICENSE , and . /g-33-L/anansi/blob/main/LICENSE-EE LICENSE-EE