rembric /ˈrem.brɪk/ — coined, from remember + fabric: the woven memory and lifecycle layer beneath your agents. One brain, one dashboard, one audit trail — shared across every MCP-capable tool: Claude Code, Codex CLI, Hermes Agent, opencode, Cursor, and beyond. The MCP memory surface is the core; sessions, judgments, consolidation, and the operator dashboard come along in the same single Node process.
Supported agents
Rembric works with any agent that speaks MCP or HTTP. First-class plugins handle session lifecycle + per-project path-scoping automatically, and redact <private>…</private> spans client-side before any transcript-derived text is uploaded (see docs/agents.md); everything else gets the same memory tools via a plain MCP URL. ChatGPT connects as a custom MCP connector over OAuth 2.1 (no static token) — see docs/agents.md.
Quickstart
One installer does everything. A single brand-styled menu prepares the server (writes docker-compose.yml + .env, generates your admin token) and installs / updates / uninstalls every agent plugin — detecting what you have and at which version. It is the recommended way to install, set up, upgrade, and remove Rembric.
curl -fsSL https://raw.githubusercontent.com/susomejias/rembric/main/install.sh | sh
Pick Server → install: it generates REMBRIC_ADMIN_TOKEN and, if Docker is present and you confirm, runs docker compose up -d for you. Then pick Plugins to install the plugin for your agent. Pin a release for reproducibility with --ref=<tag>.
Docker is the canonical distribution: the image bundles Node 22, the native modules (better-sqlite3, sqlite-vec, onnxruntime-node) and the embedding model, pre-built for linux/amd64 and linux/arm64 — the only requirement on your host is Docker and ~1 GB of RAM (2 GB recommended; the in-process embedder is why — see docs/embeddings.md).
MCP at http://<host>:8787/mcp, dashboard at http://<host>:8787/dashboard (replace <host> with 127.0.0.1 if running on the same host as your agent, or the LAN/Tailscale hostname of the box hosting Rembric otherwise).
curl -fsSL https://raw.githubusercontent.com/susomejias/rembric/main/install.sh -o rembric-install.sh
less rembric-install.sh # skim it
sh rembric-install.sh
Set up the server by hand
Prefer to skip the installer? Fetch the compose file and env template and bring it up yourself:
mkdir rembric && cd rembric
curl -fsSLO https://raw.githubusercontent.com/susomejias/rembric/main/docker-compose.yml
curl -fsSL https://raw.githubusercontent.com/susomejias/rembric/main/.env.example -o .env
# edit .env: set REMBRIC_ADMIN_TOKEN (e.g. `openssl rand -hex 32`) — that's it
docker compose up -d
docker compose logs -f rembric
Running on a remote host (LXC, NAS, server) — the canonical case
The compose file publishes port 8787 on all interfaces of the host so your agent on another machine can reach it. Point the plugin at http://<host-ip>:8787 (LAN) or http://rembric.tailnet:8787 (Tailscale). Don't expose port 8787 directly to the public internet — front it with Tailscale, WireGuard, or your reverse proxy of choice. The bearer token is the real security boundary; every endpoint requires Authorization: Bearer <token>.
Running on the same host as your agent — loopback override
If you want to restrict the published port to loopback (stricter posture, same-host dev only), drop a docker-compose.override.yml next to the canonical compose:
services:
rembric:
ports: !override
- '127.0.0.1:${REMBRIC_PORT:-8787}:8787'
Compose auto-merges the override on every up. Point your agent at http://127.0.0.1:8787. See docs/docker.md for the full topology guide.
Upgrading
Auto-updater from the UI. The dashboard checks for new releases (once a day, opt-out) and shows a badge + changelog modal when one is out. Mount the Docker socket and the same modal gains a one-click Update button that backs up the database, pulls the new image, swaps the container with health-check and rollback, and reloads the page on the new version — see docs/updates.md for setup and the security trade-off. Without the socket nothing breaks: you get the notification plus the manual flow below.
Docker manages versions for you. With REMBRIC_VERSION unset in .env, the compose file pulls :latest:
docker compose pull
docker compose up -d
Portainer / Arcane detect the new digest automatically and offer a "Recreate container" button — one click. For reproducible deploys, pin a specific version in .env (this also disables the one-click updater — the dashboard explains why):
REMBRIC_VERSION=0.21.9
Rolling back
Bump REMBRIC_VERSION to a previous tag in .env and re-run docker compose up -d. The bind-mounted ./data/ directory is untouched, so your memory stays intact across version flips.
See docs/docker.md for the full operator guide (private GHCR auth, named-volume vs bind-mount, troubleshooting).
Backups
# while the container is running (WAL-safe online backup):
docker compose exec rembric sqlite3 /data/data.db ".backup /data/backup-$(date +%Y%m%d).db"
mv ./data/backup-*.db ./backups/
# or stop + copy for a cold backup:
docker compose down
cp ./data/data.db ./backups/data-$(date +%Y%m%d).db
docker compose up -d
Do not bind-mount ./data/ onto NFS / SMB / network filesystems — SQLite's POSIX locking guarantees don't hold there, and you'll eventually corrupt the DB.
Configuration
All config via environment variables. With Docker, these live in .env and are loaded automatically by docker compose up. Required: REMBRIC_ADMIN_TOKEN (used to log into the dashboard and mint other tokens).
| Variable | Default | Description |
|---|---|---|
REMBRIC_HOST |
127.0.0.1 (0.0.0.0 in Docker) |
Bind address. Pinned to 0.0.0.0 inside the container so the published port works; never override in Docker. |
REMBRIC_PORT |
8787 |
Bind port. |
REMBRIC_DATA_DIR |
~/.rembric (/data in Docker) |
Where the SQLite file lives. Pinned to /data inside the container; bind-mount ./data:/data in compose. |
LOG_LEVEL |
info |
debug / info / warn / error. |
REMBRIC_UPDATE_CHECK |
on |
off disables the daily release check (no badge, no modal, no GitHub API call). See docs/updates.md. |
REMBRIC_PUBLIC_URL |
unset | Set to the public https issuer (http://localhost allowed for local testing) to enable the OAuth 2.1 authorization server, so OAuth clients (Claude Code, ChatGPT) connect without a static token. Off when unset. See docs/agents.md. |
Hardware requirements
Minimum 1 GB RAM; 2 GB recommended. The server embeds its semantic engine in-process — gte-multilingual-base (Apache 2.0, ONNX q8, 768 dims), loaded at boot (~1.1 s from the baked image; a broken model fails the boot instead of degrading silently), ~730 MB total process RSS measured under embedding load, ~14 ms per embedding on CPU. That requirement buys the entire trade: no external services, no API keys, no network calls — semantic candidate detection (including cross-language matching) works out of the box on an air-gapped box. Disk: the image carries the model (+~300 MB). The full pipeline is diagrammed in docs/embeddings.md.
The engine is code, not configuration: there is no model selector, no threshold knob, no off switch. The model class is pinned (≤350M params, ≤800 MB RSS); changing it is a breaking architectural change.
Consolidation sweep
Deterministic — decay + deadline orphaning, no LLM, no cron. Runs on session start (throttled to one run per scope per 6h) and on demand via the dashboard or POST /admin/consolidation/run. Pre-0.21/0.22 vars (CONSOLIDATION_*, OPENAI_*, LLM_PROVIDER, EMBEDDING_*, CANDIDATE_*_THRESHOLD) are ignored with a boot warning.
| Variable | Default | Description |
|---|---|---|
JUDGMENT_ORPHAN_AFTER_MS |
86400000 |
Age (ms) past which pending judgments surface in memory.context for the agent to close. 24h. |
JUDGMENT_ORPHAN_DEADLINE_MS |
1209600000 |
Age (ms) past which unjudged pendings are orphaned by the sweep (journaled, undoable). 14 days. |
Rate limiting
Per-token token-bucket limiter on /mcp requests. Disabled by default — single-user localhost deployments don't need it. Enable when exposing the server to multiple agents that might burst-call.
| Variable | Default | Description |
|---|---|---|
RATE_LIMIT_ENABLED |
false |
Master toggle. true activates the limiter; misbehaving tokens get 429 rate_limited with Retry-After. |
RATE_LIMIT_RPS |
10 |
Refill rate per token, in requests per second. |
RATE_LIMIT_BURST |
30 |
Burst capacity per token. The first N requests after a quiet period are free; beyond that, RPS applies. |
Security hardening
Applied to every bearer-authenticated surface (/mcp, /api, /healthz, /admin, /dashboard/login). The failed-attempt lockout is always on (it only ever penalises repeated authentication failures from one network identity, and runs before the token-hash check so bogus bearers can't exhaust CPU).
| Variable | Default | Description |
|---|---|---|
AUTH_LOCKOUT_MAX_FAILURES |
10 |
Failed auth attempts from one IP within the window before it is locked out (429). |
AUTH_LOCKOUT_WINDOW_MS |
60000 |
Window (ms) over which failures accumulate. |
AUTH_LOCKOUT_MS |
60000 |
Lockout duration (ms) once tripped. A successful auth clears the identity's counter. |
MAX_BODY_BYTES |
4194304 |
Max raw request body on /mcp and /api; larger is rejected with 413. |
REMBRIC_MCP_ALLOWED_HOSTS |
unset | Comma-separated Host allow-list (incl. port) for opt-in DNS-rebinding protection on the MCP transport. When set, requests with an unlisted Host are rejected. |
REMBRIC_MCP_ALLOWED_ORIGINS |
unset | Comma-separated Origin allow-list. When set, a request whose Origin is present and unlisted is rejected. Setting either allow-list enables the transport's rebinding check. |
OAuth token confinement. An OAuth grant is bound to the project it was consented for (the connector's /mcp/<slug> path, carried as the RFC 8707 resource indicator): the minted token authorizes only that project (project:<id> / read:project:<id>), or global scope for a path-less /mcp grant. Deploying this version revokes all previously-issued OAuth tokens, so existing OAuth clients re-authorize once (static tokens are unaffected). The dashboard session cookie is marked Secure when REMBRIC_PUBLIC_URL is https.
Sessions
| Variable | Default | Description |
|---|---|---|
SESSION_ABANDON_AFTER_MS |
86400000 |
At server startup, sessions with status='active' whose started_at is older than this are flipped to abandoned. Default 24h; floor 1min, ceiling 30 days. |
Candidate detection (save-time judgment)
Controls how memory.save surfaces conflict candidates to the agent for fresh-context judgment.
| Variable | Default | Description |
|---|---|---|
CANDIDATES_PER_SAVE_MAX |
5 |
Max number of similar memories surfaced per save. 0 disables surfacing (pending rows are still inserted and re-surface via memory.context). |
CANDIDATE_VEC_THRESHOLD |
0.85 |
Cosine-similarity floor on the embedding match. Range 0..1. Lower = more candidates, more noise. |
CANDIDATE_FTS_THRESHOLD |
0.4 |
BM25-derived score floor on the FTS5 match. Range 0..1. |
Dashboard
Self-hosted operator surface for every memory, session, prompt, judgment, and consolidation op. SSR HTML + HTMX, brutalist on purpose, no JS framework, no telemetry. Auth is one admin token — no onboarding flow.
Day-to-day operator work lives in the dashboard at http://127.0.0.1:8787/dashboard (port 8788 in the dev stack). Mint and revoke API tokens at /dashboard/tokens, create and archive projects at /dashboard/projects, soft-delete agent sessions at /dashboard/sessions, browse and soft-delete curated user prompts at /dashboard/prompts (FTS5 search over content + tags; refined predecessors show a REFINED badge), trigger consolidation at /dashboard/consolidation, and run the operator-only purges from /dashboard/maintenance. Programmatic agents talk to the same data through the MCP project.* / memory.* tools or, for admin-only operations, through the HTTP endpoints under /admin/* (admin bearer token required — see docs/agents.md).
Maintenance (manual purges)
The dashboard exposes /dashboard/maintenance for three irreversible, operator-triggered physical purges. All are gated to dashboard sessions whose underlying token has scope * (admin):
- Purge empty sessions — removes
ended/abandonedsession rows that have no summary, no manual title, no referencing memories / non-deleted prompts / confirmations, are not operator-soft-deleted, and ended over 1 hour ago. Soft-deleted prompts no longer block purge. - Purge disconnected archived memories — removes
archivedmemory rows whose ids are referenced by NO other row in the graph (memory.replaces,consolidation_ops.affected_ids/created_id,memory_relations.{source,target}_id,confirmations.memory_id). The matchingmemory_vecandmemory_ftsshadow rows are dropped in the same transaction. - Purge deleted prompts — removes
promptsrows whosedeleted_at IS NOT NULL. Covers both operator soft-deletes (from/dashboard/prompts) and refine supersedes (frommemory.save_prompt({ replaces })). The matchingprompts_ftsshadow row is dropped in the same transaction.
Each click shows a count and a confirmation modal. The deletion is journaled in consolidation_ops (op_type = 'session_purge', 'archived_memory_purge', or 'prompt_purge') so the operator can audit what was removed even after the rows are gone. Consolidation undo on operations whose rows have been purged returns a structured error — the dashboard surfaces it inline naming the missing ids.
To reclaim file-level disk after a large purge, run VACUUM against the SQLite file. The maintenance page surfaces the freelist size so you know how much would be reclaimed.
Architecture
Single Node process, single SQLite file, packaged as a multi-arch Docker image (linux/amd64, linux/arm64). Agents reach it over HTTP(S) + a bearer token at /mcp/<slug> (or /mcp + project.use); the operator dashboard is served from the same process.
Four load-bearing invariants:
- Append-only: rows are never deleted;
contentnever updated. Lifecycle isstatusflips +replaceslinks. Every consolidation op is reversible. An agent MAY retire a memory at the user's explicit request viamemory.archive(a reversible, journaledactive → archivedflip, scoped to the connection — no successor link, distinct from atopic_key/memory.judgesupersede); physical purge stays operator-only in/dashboard/maintenance. - Project scoping by construction: every memory is
globalor attached to oneproject_id. Consolidation and relations never cross scope. - Convergent topics via
topic_key: onmemory.save, the previously-active row in the same(scope, project_id, topic_key)is auto-superseded atomically. - Fresh-context judgment: candidate conflicts surface at save time (
candidates[]); the agent that produced the conflict judges it. Aged pendings re-surface inmemory.contextuntil an agent closes them. The deterministic sweep (no LLM, no cron — runs on session start) only handles decay + deadline orphaning.
Memory also exposes a derived review state alongside decay: a needs_review flag on memory.search / memory.get rows (and a needsReview[] list in memory.context) for active memories whose per-type shelf life elapsed without re-affirmation. It is computed at read time — no column, no sweep — and is cleared by re-affirming with memory.confirm (not by merely reading). See docs/relations.md for the relation taxonomy and the review axis.
┌─────────────────────────────────────────────────┐
│ Agents (MCP clients) │
│ Claude Code · Codex CLI · Cursor · … │
└────────────────────────┬────────────────────────┘
│
│ HTTP(S) + Bearer token
│ URL path: /mcp/<slug> (or /mcp + project.use)
▼
┌───────────────────────────────────────────────────────────────────────┐
│ rembric (single Node process) │
│ │
│ ┌────────────────────────────┐ ┌───────────────────────────────┐ │
│ │ /mcp /mcp/<slug> │ │ /dashboard │ │
│ │ Streamable HTTP transport │ │ SSR HTML + HTMX │ │
│ │ + initialize.instructions │ │ │ │
│ │ memory.{save,search,…} │ │ /memories /sessions │ │
│ │ memory.session_* │ │ /prompts /judgments │ │
│ │ memory.*_prompt(s) │ │ /consolidation /projects │ │
│ │ memory.judge / compare │ │ /tokens /maintenance │ │
│ │ project.{use,list,current}│ │ │ │
│ └─────────────┬──────────────┘ └─────────────┬─────────────────┘ │
│ ▼ ▼ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ Service layer │ │
│ │ MemoryService · PromptsService · RelationsService │ │
│ │ ProjectsService · TokensService · AgentSessionsService │ │
│ │ SessionRouter │ │
│ └───────────────────────────────┬───────────────────────────────┘ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ SQLite (Drizzle, append-only + tombstones) │ │
│ │ memory · projects · tokens · sessions · prompts │ │
│ │ memory_relations · consolidation_{runs,ops} │ │
│ │ + memory_fts · prompts_fts (FTS5) + memory_vec (sqlite-vec)│ │
│ └───────────────────────────────▲───────────────────────────────┘ │
│ ┌───────────────────────────────┴───────────────────────────────┐ │
│ │ In-process background work (no external services) │ │
│ │ embedder: gte-multilingual-base, ONNX q8, loaded at boot │ │
│ │ drain worker (every 30s) fills memory_vec │ │
│ │ deterministic sweep: decay + deadline orphaning │ │
│ └───────────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────────┘
Two staleness axes, deliberately distinct: decay runs in the background sweep above (access + confidence, keyed on last_seen_at → archives); review is derived in the request path on memory.search / memory.get / memory.context (affirmation, keyed on max(created_at, last confirmation) + per-type TTL → flags needs_review). Review adds no table, no tool, and no background work — it is computed at read time and cleared by memory.confirm.
Development
pnpm install
pnpm run dev # tsc --watch
pnpm test # full vitest suite + Hermes Python tests
pnpm run typecheck # tsc --noEmit
pnpm run lint
For a clean Docker build from source:
docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build
CodeGraph (code intelligence)
This repo ships a committed CodeGraph setup so agents can query the codebase as a knowledge graph. The MCP server is registered in .mcp.json (Claude Code) and opencode.jsonc (opencode); both point at a local codegraph binary.
CodeGraph is opt-in per developer — install the binary to enable it:
# install the codegraph CLI, then from the repo root:
codegraph index # builds .codegraph/codegraph.db (git-ignored, ~11 MB)
If you do not install the binary, nothing breaks: the codegraph MCP entry just shows as failed/unavailable in /mcp and every other workflow is unaffected. The index, daemon socket, pid, and logs under .codegraph/ are git-ignored (only .codegraph/.gitignore is committed) — they are local to each machine and never belong in a commit.
More docs
- docs/docker.md — Docker operator guide (topologies, GHCR auth, upgrade/rollback, troubleshooting).
- docs/agents.md — per-client MCP config (Codex, Cursor, Windsurf, VS Code, Gemini, …).
- docs/backup.md — backup strategies and restore recipes for the SQLite data file.
- docs/updates.md — update notifications and the opt-in one-click auto-updater from the dashboard.
- docs/troubleshooting.md — common errors and recovery.
Project status
Rembric is open-source under the MIT License — as-is, no warranty. The maintainers do not accept responsibility for operator data loss, deployment downtime, or any other operational consequence of running the software.
- Security: vulnerability reports go through SECURITY.md. Preferred channel is GitHub Security Advisories.
- Backups: the entire server state lives in one SQLite file. Operators are responsible for backing it up; see docs/backup.md for
sqlite3 .backupcron, manual snapshots, and litestream. - Versioning: SemVer, driven by release-please. Pre-1.0 means the wire and HTTP contracts may shift across minors; check release notes before upgrading.
Contributing
See CONTRIBUTING.md. Commits follow Conventional Commits; pre-commit lints and typechecks; pre-push runs the full test suite.
Sponsor
If Rembric saves you time, you can support its maintenance through GitHub Sponsors, Ko-fi, or Buy Me a Coffee.
License
MIT — see LICENSE.
No comments yet
Be the first to share your take.