Nubase
Get started

Memory

A durable, queryable, evolving knowledge base per user — the fourth primitive next to Database, Storage and Auth. mem0-compatible REST surface, multi-signal retrieval, full audit history, per-tenant isolation.

What it does, in one paragraph

You hand it conversation messages. It (a) asks an LLM to extract durable facts and entities, (b) decides — fact by fact — whether each should be added, used to update an existing memory, used to delete a now-obsolete one, or skipped because we already know it. Writes go into a per-tenant mem.memories table (with pgvector embeddings + entity links + audit history). Reads fuse vector cosine, BM25 keyword search and entity-link boost into a single ranked list.

Data layout

  • mem.memories — fact text + vector(N) embedding + owner triple + metadata
  • mem.memory_history — append-only ADD/UPDATE/DELETE audit log
  • mem.entities — extracted entities with linked_memory_ids[]
  • mem.session_messages — rolling N-message window per conversation

Retrieval pipeline

  1. Embed query (cached in-process via Caffeine)
  2. Vector search top-K with cosine distance (pgvector HNSW index)
  3. BM25 keyword search using ts_rank_cd against a GIN index
  4. Entity boost: extract query entities, look them up in mem.entities, distribute spread-attenuated boost to linked memories
  5. Fuse: combined = (sim + bm25_norm + entity_boost) / max_possible
  6. Return top-K ranked

Authorization

Every mem endpoint goes through MemoryAuthScope:

  • service_role apikey — sees the whole tenant; can pin any userId
  • Authenticated user — userId is forced from the JWT sub; if the body asks for a different userId, 403
  • Anonymous (apikey only, no Bearer) — refused

Defense in depth: the repository layer also takes the owner into the WHERE clause (findByIdForOwner), so even if the service layer ever has a bug, the SQL refuses cross-owner reads.

Providers

Chat LLM and embedding provider are independent, both configurable via application.yml:

nubase:
  mem:
    enabled: true
    chat-provider: openai          # openai | anthropic | generic
    embedding-provider: openai     # openai | generic
    embedding:
      model: text-embedding-3-small
      dimensions: 1536             # baked into vector(N) at tenant init
    chat:
      model: gpt-4o-mini
    search:
      default-top-k: 5
      entity-boost-enabled: true
      fts-config: simple           # english / zhparser for Chinese

The generic provider talks any OpenAI-compatible API: DashScope, DeepSeek, Moonshot, vLLM, Ollama. Anthropic uses the Claude Messages API and translates the system message + JSON mode automatically.

Operations

  • Migrate existing tenants: POST /auth/v1/admin/init/mem-schema?dbKey=<tenant> — idempotent, backfills the mem.* schema and grants on tenants that predate the memory module.
  • Reset: POST /mem/v1/reset (service_role) truncates every mem.* table for the current tenant. The Studio Settings page gates this behind a type-the-project-name confirmation.
  • Disable globally: Set nubase.mem.enabled=false — API returns 404, new tenants skip mem schema entirely, migrate endpoint refuses.
  • pgvector required: when enabled=true the tenant init fail-fasts if pgvector isn't available on the Postgres server. Recommended image: pgvector/pgvector:pg15.