Utopia's Append-Only Decision Ledger Runs as the Role That Can Delete It
A 27-day-old Rust and Postgres knowledge graph promises agents an immutable audit trail. The immutability is enforced by database triggers, and the deployment it ships with can drop them.
Utopia's pitch is a record that outlives the thing it describes. Confirm a fact, reject one, merge two entities, rebuild the graph, and each action "leaves a record of who, when, and what the object looked like at the time." The ledger is append-only. A record survives even the knowledge base it belonged to.
Then you open .env.example and find a comment, in Chinese, explaining why an optional restricted database role exists. My translation: the owner role can DROP TRIGGER, and so can bypass the ledger's immutability constraint.
The append-only guarantee is enforced by Postgres triggers. The default deployment connects as the role that owns those triggers. Nothing in the quickstart tells you to change that.
Why this is the thing to test, not a footnote
3,223 stars in 27 days, which is what got it onto Trendshift this morning. The repo was created August 7 and pushed to within hours of my reading it.
But star velocity is not why I picked this apart. I picked it apart because of what Utopia is for. If you want vectors, you have twenty options. If you want a graph, you have twenty more. The reason to choose this one is the sentence in its own philosophy section:
Where existing vector stores and knowledge graphs work to get present knowledge right, one of Utopia's founding aims is to record the whole course of changing understanding.
That is the differentiator. An agent that can answer "what did you believe on August 12, and what have you learned since" is a different kind of system than one that answers "what is true now." The project calls the engineered version of that a bitemporal knowledge graph, and the README's definition is compact enough to quote whole: "Correcting a fact closes the old version and links the new one to it rather than overwriting, so the graph keeps two timelines: when something was true in the world, and when the system came to believe it."
When the entire value proposition is a trustworthy record, the record's integrity is the product. Everything else is implementation detail. So that is where I went first.
How the ledger actually holds, and where it does not
The mechanism is Postgres triggers on the ledger tables. That is a reasonable choice, and stronger than enforcing append-only in Rust and hoping every code path agrees. Triggers sit below the application, so a bug, a bad migration, or a rogue query through the same connection cannot write around them.
The gap is who the application connects as.
Utopia supports two roles. There is the owner role, which creates the schema and owns every object in it. There is an optional restricted runtime role, which you turn on by setting UTOPIA_APP_DB_PASSWORD and pointing UTOPIA_MIGRATION_URL at the owner for migrations. SECURITY.md lists "optional least-privilege runtime role" in its "what is in place" section, and the word doing the work is optional.
Run docker compose --profile app up -d from the README quickstart and you get the single-role path. The application holds owner privileges. An owner can drop a trigger. Drop the trigger and the ledger is an ordinary table with an append-only naming convention.
To be fair to the project, and I want to be, this is not hidden. The comment I translated is the project explaining its own reasoning, and the same file is honest about how far the restricted role gets you: it blocks application bugs, SQL injection, and leaked connection strings, but not anyone who can log into the host, because psql inside the container is passwordless superuser. That is an accurate scoping statement, and most projects at this maturity do not write one.
What is missing is the connection between the two facts. The README sells an append-only ledger in its feature table. The mitigation that makes it append-only in practice lives in an example environment file, in a language most of the repo's English-reading audience does not read, marked optional. Someone will deploy this for a compliance audit trail and never learn those are the same subject.
The engineering underneath is better than the packaging
I want to be clear that I like this repo, and the reason is that its documentation keeps doing things I almost never see.
Entity resolution runs on two hard-coded thresholds, SIM_ATTACH = 0.55 and SIM_NEW = 0.35, with an explicit bias: "Prefer splitting over merging. A wrong merge mixes two entities' facts together, which costs far more than one extra entity." That is a real engineering position with a stated cost function, not a knob dressed up as intelligence.
The benchmark writeup is the part that convinced me. They run the first six Sherlock Holmes stories and publish a table where entities correctly merged goes 14, then 37, then 47, then 57 across four runs as three separate bugs got fixed. Their own gloss:
Each layer hid the next. The value of the benchmark corpus is the next layer it exposes after every fix, more than the first number it produces.
They also refuse to publish an accuracy key for that corpus, on the grounds that the model has read the book, so a type-accuracy score would measure memory rather than the pipeline. And items queued for human review are counted as misses, not hits, because counting them as hits credits a person's work to the machine.
Extraction failures get eleven distinct reason codes recorded in a table and surfaced in the UI. Reasoning derivation ships off by default, "since a wrong axiom derives wrong facts," and an asserted fact beats a derived one when they conflict.
The MCP surface is the same kind of careful. Five read-only tools are exposed: search_chunks, search_docs, find_entities, changes, and entity_facts. Two more exist internally and are deliberately withheld, query_data (which runs SQL against a mounted production database) and remember (which writes to the ledger). Even a write-scoped token gets can_write: false and no mounted sources over MCP. The source comment translates roughly to "scope is a ceiling, not an authorization," and every tool call writes an audit row.
So the mismatch is not competence. It is that the security-relevant reasoning lives in code comments and an example env file while the marketing lives in the README table.
Put this into practice
Stand it up in about ten minutes, then spend twenty more before you point real documents at it.
-
Get it running.
git clone https://github.com/deeplethe/utopia.git, thencd utopia, thendocker compose --profile app up -d. Openhttp://localhost:1516and register. The first account becomes the administrator automatically. You need Docker; local development also wants Rust 1.85+, Node 20+ and pnpm. -
Pin the image before you do anything else. Set
UTOPIA_IMAGEin.envto an explicit tag. The README says so directly, and there is a sharp reason: the current release isv0.1.0-rc3, tagged this morning at 07:22 UTC, and the release notes state thatlatestdeliberately does not point at it, because "a candidate that takeslatestis not a candidate." -
Change the database password. SECURITY.md: "The default database password is
utopia." It binds to127.0.0.1:1517by default, so nothing outside the host reaches it. If you ever touchUTOPIA_DB_BIND, changeUTOPIA_DB_PASSWORDfirst. -
Turn on the restricted role if the ledger is why you are here. Set
UTOPIA_APP_DB_PASSWORDand pointUTOPIA_MIGRATION_URLat the owner connection so migrations still run. This is the step that turns the append-only claim into something the application cannot undo. Skip it and you have a log, not a ledger. -
Point an LLM endpoint at it before ingesting. Any OpenAI-compatible endpoint works, including Ollama and vLLM, so the whole thing runs air-gapped. Extraction will not start without chat and embedding endpoints configured in system settings.
-
Feed it something with real timestamps. The ingest docs note that
doc_timedrives the time axis and documents without it fall back to ingestion time. On a bitemporal system that fallback ruins the axis you came for, and nothing warns you. -
Back up
./dataand the database together before every upgrade. Thedatadirectory holds raw files and the Tantivy index; the graph is in Postgres. Restoring one without the other leaves you with a search index that disagrees with the store.
Honest limitations
This is v0.1.0-rc3, and the version number is not modesty. Three release candidates shipped in roughly eighteen hours between September 2 and this morning. The original 0.1.0 was withdrawn entirely, tag, GitHub release and container images deleted. The default branch is dev, not main, so a naive clone lands you on the integration branch.
Schema migrations only roll forward, with no rollback. The contributing guide adds a related trap: two branches each writing an 0011_ migration has happened, and after that merge neither one ran. On a system whose selling point is a permanent record, a forward-only migration path with a history of numbering collisions deserves a hard look before production.
Credentials sit in Postgres in cleartext. LLM API keys and data-source connection strings live unencrypted in llm_settings.chat_api_key and data_sources.conn_string. SECURITY.md says encryption at rest is a 1.0 item and to keep the system inside a trusted network until then.
The rc3 release fixed a credential leak that had been live: until this morning, only the custom fetcher's auth_header was stripped from API responses, so S3, Azure and GCS keys, the WebDAV password and the Notion token were being returned to every viewer of a knowledge base. Reported externally, fixed within a day, disclosed in the release notes. Good handling. Also a reminder of how young this code is.
Temporal precision is coarse. Timestamps round to a UTC day today, which the roadmap admits "can shift an event across midnight by one day." If your use case is reconstructing a decision sequence within a business day, that is disqualifying until instant precision lands. Type resolution also runs by hand only, with no automatic trigger, which the pipeline doc calls "a real gap."
Two claims I did not verify. The Ontology2SQL "state of the art on BIRD Mini-Dev" line is the project's own, and I did not open the leaderboard submission. And api.github.com was unreachable from my sandbox, so stars, forks and dates came from shields.io JSON and a GitHub proxy that agreed with each other rather than from GitHub first-party. No documented RAM requirement exists anywhere in the repo, so plan capacity by testing rather than by any number you read, including here.
What I would want answered before I trusted it with an audit
The interesting question Utopia raises is not about Utopia. It is what an "immutable" record means when a database owner can always drop the trigger, a host operator can always reach psql, and the only backstop is a role configuration marked optional in an example file.
Every append-only store has this shape somewhere. Utopia is more honest than most about where its shape is. The right response is not to hold that against it but to ask the same question of whatever you are running now, and to notice whether your current system documents its own escape hatch at all.
If you deploy this and check whether the ledger triggers survive an upgrade under the restricted role, I would like to know what happens. Nobody seems to have tested that in public yet, and a forward-only migration path against trigger-enforced immutability is exactly the seam where audit trails go wrong.
Sources: deeplethe/utopia README, SECURITY.md, CONTRIBUTING.md, docs/pipeline.md and .env.example, fetched raw with cache-busting on 2026-09-03; v0.1.0-rc3 release notes. Star, fork and date figures from cache-busted shields.io JSON and a GitHub metadata proxy. Chinese source comments are my translation and are marked as such in the text.