Local-first encrypted memory for AI agents.

Store an agent's memory on-device, recall it with hybrid vector search, and erase it by destroying its key, with a verifiable erasure receipt. Zero-LLM ingest and an MCP server, all on one encrypted SQL/vector file. No API keys; your data stays on your machine.

Argon2id/AES-256-CTR/HMAC-SHA256/BLAKE3 Merkle/PRISM ANN
$cargo add citadeldb citadeldb-mem

A live model of Citadel's on-disk format: my.db is opaque ciphertext until the passphrase decrypts the page. Nothing is stored - to run real SQL, open the playground.

my.dbsealed
1 page / 8,208 B / IV ‖ ciphertext ‖ MAC
$
Argon2id / AES-256-CTR / HMAC-SHA256 / enter passphrase to decrypt
// proven in
87.2% historical LoCoMo score94.5% historical recall@50Thousands of tests, 22 crates59 SQL comparisons vs SQLite
Desktop client
Citadel Studio logo

Citadel Studio.

A native client for Windows, macOS, and Linux. Open encrypted vaults, browse tables and stored memory, run SQL with EXPLAIN and ANALYZE, and inspect vectors and integrity results.

Citadel Studio showing stored memory and integrity results
One substrate, two faces

An encrypted file that is both an agent memory and a database.

The same AES-256-CTR + HMAC pages, copy-on-write B+ tree, and shadow-paging commit power both halves. Use the memory engine, the SQL engine, or both in one file.

citadeldb-vector / -mem / -llm / -ai / -mcp

Memory engine

Memory that lives encrypted at rest. Typed atoms and edges, hybrid recall, and a Model Context Protocol server so Claude Desktop or any MCP client can read and write it.

  • VECTOR(N) type with a PRISM-backed filtered ANN index
  • Hybrid recall: vector ANN + BM25 keyword + recency + optional cross-encoder reranker
  • Cryptographic forgetting: erase an atom or region by destroying its key
  • 15-tool MCP server; historical scores of 87.2% on LoCoMo and 86.2% on LongMemEval-S (gpt-4o)
citadeldb / citadeldb-sql

Encrypted database

An embedded SQL + key-value engine with snapshot isolation and shadow paging. Encrypted pages live in the database file; encryption keys live in a companion key file.

  • SQL: FULL OUTER & LATERAL joins, recursive CTEs, window functions, triggers, materialized views
  • JSON / JSONB with 14 PostgreSQL operators, GIN indexes, full-text search
  • Key-value API and SQL share one transaction
  • P2P encrypted sync over Noise; Python, CLI, C FFI, and WebAssembly bindings
Features

A complete engine in a single file.

Full SQL, real ACID, encrypted sync, and a memory engine. Key hierarchy built around RFC 3394, RFC 5869, and a Rust-only crypto core.

Encrypted at rest

AES-256-CTR + HMAC-SHA256 per page, verified before decryption. Fresh random IV on every write.

Real SQL

FULL OUTER + LATERAL joins, recursive CTEs (with-DML), window functions, triggers, materialized views, UPSERT, RETURNING, JSON/JSONB.

ACID, no WAL

Copy-on-Write B+ tree with shadow paging. Snapshot isolation with concurrent readers, atomic single-byte commits.

Vector + memory engine

VECTOR(N) with a PRISM filtered ANN index, plus a memory engine of typed atoms with hybrid vector + keyword recall.

MCP server

Expose encrypted memory to Claude Desktop or any MCP client over stdio. 15 tools for recall, remember, link, evolve, and forget.

Cryptographic forgetting

Erase data by destroying its key, not by overwriting. Whole-store, per-region, and per-atom, with verifiable erasure receipts.

P2P encrypted sync

Merkle-based table diffing over Noise (NNpsk0_25519_ChaChaPoly_BLAKE2s) with ephemeral forward secrecy.

Three-tier key hierarchy

Passphrase → Argon2id → Master Key → AES-KW → REK → HKDF → DEK + MAC. Passphrase changes need no page re-encryption.

Cross-platform bindings

Windows, Linux, macOS. Python, a panic-safe C FFI, and WebAssembly from one Rust core.

Quickstart

Open an encrypted database.

Create a vault, connect to SQL, and run a query.

1

Add the crates

Engine + SQL frontend from crates.io.

2

Open with a passphrase

Argon2id derives the master key in memory. Keys live in {dbname}.citadel-keys, not inside the database.

3

Run SQL, or use the KV API

Both are first class. Mix them in the same transaction.

4

Keep the vault together

The database has a separate key file and optional key and audit sidecars. Use the backup API for a consistent snapshot.

src/main.rs
How the seal works

What "open my.db" actually does.

Six stages from passphrase to a sealed 8,208-byte page on disk. Real algorithm names, real byte counts, drawn from the citadel-crypto source.

citadel-crypto  /  page seal pipeline What happens when Citadel writes one page to disk.
stdin + keyfileGather inputs
$passphrase:
+
salt-16 B
Read 16-byte salt from my.db.citadel-keys. The passphrase stays in memory only; never touches disk.
memory-hard KDFArgon2id
params: t=3, m=64 MiB, p=4filling memory...
Uses 64 MiB of memory across three passes to derive the master key.
master key-32 B
RFC 3394AES-KW unwrap
wrapped REK40 B blob
master key
REK-32 B
Unwraps the root encryption key. Rekeying only rewraps this 40-byte blob; no pages re-encrypted.
RFC 5869 / once per openHKDF-SHA256 split
REK32 B
salt = zeros(32)
info="citadel-dek-v1"
DEK-32 B
info="citadel-mac-key-v1"
MAC key-32 B
Domain-separated expansion: one call per label. DEK, MAC key, and an audit key live in memory for the session.
AES-256-CTR + HMAC-SHA256Seal the page
IV-16 B
plaintext8,160 B
AES-256-CTR encrypts with a fresh random IV per page. HMAC signs epoch ‖ page_id ‖ IV ‖ ciphertext, so a forged page id or replay from another epoch fails the tag. Page = 16 + 8,160 + 32 = 8,208 bytes.
citadel-iofsync to disk
my.db appended page #42 / IV ‖ CT ‖ MAC+ 8,208 B
my.db.citadel-keys unchanged / salt + wrapped REK + file MAC172 B
Two files, two secrets. Steal my.db alone and you get opaque ciphertext. Lose the passphrase and nothing opens.
One derivation chain, one sealed page. The passphrase never touches disk. Argon2id re-derives the master key in memory on every open.
keys derived once / seals page 42
Memory engine

Encrypted memory engine.

citadel-mem stores memory as typed atoms grouped into regions and connected by typed edges. Recall blends vector similarity, keyword scoring, and recency; forgetting destroys keys, not just rows.

remember
Store an atom with its embedding, payload, score, and TTL.
recall
Hybrid retrieval: ANN + BM25 + recency + optional reranker.
fetch
Deterministic listing by kind, no ranking.
update
Replace an atom's payload in place.
link
Typed directed edges between atoms; cycles rejected on DAG kinds.
evolve
Recompute neighbors and decay scores over time.
summarize
Per-kind digest of a region.
evict
Selective forgetting by policy.
forget
Cryptographic erasure with a verifiable receipt.

Hybrid fusion recall. A query over-fetches ANN candidates, then scores each on normalized semantic distance, BM25 keyword overlap, recency (30-day half-life), and importance. An optional cross-encoder reranker (replace or reciprocal-rank-fusion) sharpens the top results, and a graph walk pulls in linked neighbors.

Cryptographic forgetting. Encrypted regions seal atoms under individual keys. Forgetting destroys the active vault's key copies and returns a receipt. External backups, replicas, and physical-media copies are outside that scope.

Explicit semantic embeddings. Use local Candle models such as E5-large or BGE, or a custom embedding backend. Cross-encoder reranking and CUDA support are optional. Stored-memory inspection through MemoryMaintenance needs no model.

87.2%
LoCoMo (gpt-4o-mini)
86.2%
LongMemEval-S (gpt-4o)
94.5%
Historical LoCoMo recall@50
8
typed edge kinds

Historical results on encrypted regions. LoCoMo uses the harness's prompts with a gpt-4o-mini reader and judge. LongMemEval-S uses the official CoT reader prompt and judge protocol with gpt-4o. Recorded configurations and limitations.

Model Context Protocol

Plug encrypted memory into Claude Desktop.

citadeldb-mcp exposes a memory region as 15 MCP tools over JSON-RPC 2.0 on stdio, so Claude Desktop, an IDE, or any other MCP client can read and write it directly.

Read tools
  • mem_recall
  • mem_fetch
  • mem_get
  • mem_edges
  • mem_profile
  • mem_summarize
  • mem_verify
Write & forget tools
  • mem_remember
  • mem_remember_batch
  • mem_update
  • mem_link
  • mem_unlink
  • mem_evolve
  • mem_evict
  • mem_forget
claude_desktop_config.json
{
  "mcpServers": {
    "citadel": {
      "command": "citadeldb-mcp",
      "args": [
        "--db", "/absolute/path/to/memory.cdl",
        "--embedder", "e5-large",
        "--reranker", "ms-marco-minilm"
      ],
      "env": { "CITADEL_KEY": "your-passphrase" }
    }
  }
}

Install with pip install citadeldb-mcp, then run citadeldb-mcp pull e5-large and citadeldb-mcp pull ms-marco-minilm. Pulls need no vault key; serving requires CITADEL_KEY, --db, and --embedder. Managed model downloads are pinned and verified. Recall hits include provenance and integrity results. See MCP setup.

Benchmarks

59 SQL comparisons with SQLite.

Single-threaded measurements with durability off and both caches configured for 4,096 pages (about 32 MiB). Ratios are SQLite time / Citadel time: above 1 means Citadel is faster, below 1 means Citadel is slower. Execution and cached repeat reads are reported separately. Measurements from September 13, 2026 combine the complete 6b41d0c3 run with eight execution pairs from ea8827d7 and three UPDATE pairs from 0f9362cf, as listed in the methodology; they are not a full-suite timing run at one revision. 58 of 59 aggregate point ratios exceed 1; generated UPDATE is 0.960×, with run-level uncertainty retained in the data.

37
execution comparisons
22
cached-read comparisons
2
candidate runs per measurement
30
samples per run

Execution speed

Benchmark
Relative speed
Citadel
SQLite
Ratio

Cached repeat reads

Identical reads reuse Citadel's cached results or projected UNION ALL branches while SQLite executes again. These are not first-execution timings.

Benchmark
Relative speed
Citadel
SQLite
Ratio
SQLite: page_size=8192, journal_mode=MEMORY, synchronous=OFF, cache_size=4096. Citadel: SyncMode::Off, cache_size=4096. Reproduce: cargo bench --locked -p citadeldb-sql --bench h2h_bench -- --sample-size 30 --warm-up-time 1 --measurement-time 2 --noplot. The displayed rows use the two candidate runs from serial reference/candidate/candidate/reference cohorts; the data retains per-run intervals and drift.
Architecture

22 crates, one file format.

Desktop, CLI, bindings, and agent interfaces share the database, SQL, and memory APIs. Studio uses those APIs directly, without an MCP server.

citadel-studiodesktop client
citadel-cliinteractive shell
citadel-pythonPython wheel
citadel-ffiPanic-safe C ABI
citadel-wasmbrowser builds
citadel-mcpMCP server
citadel-llmLLM client layer
citadel-aiagent runtime
citadel-membenchLoCoMo harness
citadel-swemini-SWE harness
citadel-memregions / atoms / edges
citadel-sqlparser / planner / executor
citadel-vectorVECTOR / ANN
sql-json-pathSQL/JSON paths
citadeldatabase API / builder / sync
citadel-txntransactions
citadel-syncreplication
citadel-cryptokey hierarchy / at-rest encryption
citadel-bufferSIEVE buffer pool
citadel-pagepage codec
citadel-iofile I/O / fsync / io_uring
citadel-coretypes / errors / constants

Page layout is 8,208 bytes. Sixteen bytes of random IV, 8,160 bytes of ciphertext, and 32 bytes of HMAC-SHA256. Authentication is checked before decryption.

Commit protocol is shadow paging. Dirty pages go to new locations, BLAKE3 Merkle hashes climb bottom-up, the inactive 240-byte commit slot is updated, then one byte in the file header flips to publish the new root. No write-ahead log.

Memory uses the same storage engine. Vectors, atoms, and edges use authenticated pages and database transactions. citadel-membench measures recall on LoCoMo and LongMemEval.

Commit Protocol
1 / 4  /  Copy-on-write new pages
Two dirty pages in the buffer pool are ready to commit. Before a single byte touches disk, the active slot stays untouched, so a crash right now leaves the last good snapshot intact.
citadel-buffer  in memory
page #42modified
page #43clean
page #44modified
page #45clean
SIEVE pool / 4,096 frames
encode / seal
my.db  on disk
god byte / 1 B
00000000
bit 0 / active slot A / B
slot A / 240 Bmerkle: 8a1c...e4f2active
#42
#43
#44
#45
slot B / 240 Bmerkle: -shadow
#42#43#44#45h₁₂h₃₄root
SQL

A SQL dialect that doesn't disappoint.

FULL OUTER and LATERAL joins, recursive and DML-bearing CTEs, triggers, materialized views, window frames, JSON/JSONB with PostgreSQL operators, full-text search, and a native VECTOR type.

Statements
  • CREATE / DROP / ALTER TABLE
  • CREATE INDEX (partial / expr)
  • CREATE VIEW / MATERIALIZED VIEW
  • CREATE TRIGGER
  • UPSERT (ON CONFLICT)
  • RETURNING (OLD / NEW)
  • TRUNCATE / SAVEPOINT
  • PREPARED / $1, $2, ...
  • EXPLAIN / EXPLAIN ANALYZE
Clauses & joins
  • INNER / LEFT / RIGHT / CROSS
  • FULL OUTER / LATERAL
  • Subq.: scalar / IN / EXISTS / ANY/ALL
  • Correlated subqueries
  • WITH / WITH RECURSIVE
  • WITH-DML (RETURNING)
  • UNION / INTERSECT / EXCEPT
  • GROUP BY / HAVING
  • Window frames (ROWS / RANGE)
JSON / JSONB & search
  • 14 PostgreSQL operators
  • -> / ->> / #> / #>>
  • @> / <@ / ? / ?| / ?&
  • 16 scalar / 4 aggregate fns
  • GIN indexes on JSONB
  • FTS: tsvector / ts_rank / phrase
  • VECTOR(N) + ANN index
  • <-> L2 / <#> inner / <=> cosine
Types / constraints / windows
  • INT / REAL / TEXT / BLOB / BOOL
  • DATE / TIME / TIMESTAMP(TZ)
  • INTERVAL / IANA zones (jiff)
  • Generated cols (STORED / VIRTUAL)
  • STRICT tables
  • COLLATE BINARY / NOCASE / RTRIM
  • FK: CASCADE / SET NULL / DEFERRABLE
  • ROW_NUMBER / RANK / LAG / LEAD
  • SUM / AVG OVER / PARTITION BY
Security

Encryption, keys, and integrity.

No plaintext on disk

Page payloads are encrypted before writing and authenticated before decryption. File headers and commit metadata are not hidden.

Keys live outside the database

Encryption material lives in {dbname}.citadel-keys. The passphrase derives a master key in memory via Argon2id and never touches disk.

Passphrase rotation

Changing a passphrase derives new wrapping material and re-wraps the root key. It does not re-encrypt pages; the work is independent of the database's page count.

Cryptographic erasure

Encrypted regions use per-atom keys. Forgetting destroys the active key copies and issues a receipt; it does not erase external backups, replicas, or physical-media remnants.

Forward-secret sync

Noise NNpsk0_25519_ChaChaPoly_BLAKE2s with a 256-bit PSK. Ephemeral Curve25519 keys per session; compromise of one doesn't leak prior traffic.

FIPS-oriented at-rest profile

A feature flag uses PBKDF2-HMAC-SHA256 (600,000+ iterations) with AES-256-CTR for database storage. This is not a claim of whole-product validation.

Audit verification detects edits and broken links in retained history. It cannot detect restoration of a complete, older authentic vault snapshot, including its keys and audit files. Freshness requires an external anchor. See the security policy.

Distribution

Desktop, CLI, and language bindings.

Rust

crates.io / citadeldb

The database API and SQL frontend, with typed transactions. 16 crates published.

$ cargo add citadeldb citadeldb-sql

Python

PyPI / citadeldb

Encrypted SQL, vector search, and memory with type stubs. Bring your own embedder; the standalone MCP server is a separate package.

$ pip install citadeldb

C / C++

cbindgen / citadel.h

A panic-safe C ABI with static or dynamic linkage. Drop into any toolchain that speaks C ABI.

> #include <citadel.h>

WebAssembly

npm / @citadeldb/wasm

A real encrypted database in your browser tab. Compiled with wasm-pack. Powers the playground.

$ npm install @citadeldb/wasm

CLI

crates.io / citadeldb-cli

An interactive SQL shell with tab completion, syntax highlighting, and 27 dot-commands.

$ cargo install citadeldb-cli
Writing

From the blog.

Start with the browser build.

No install, no signup. Run real SQL against an encrypted database compiled to WebAssembly.