[← Blog]

The LastDB App Registry: Install by Proof

LastDB now has an app registry. lastdb app install brain gives you the newest Brain commit that was proved with the LastDB build on your machine. The registry is one static signed file. It needs no account and no live service. This post says what the registry is, how a row gets into it, and how you start.

What went wrong without it

LastDB is a local database. The apps we build on it — Brain (a knowledge base), Kanban (a board), Situations (shared operational posture), Search, Lastsecrets, and a few more — are separate repositories. Until this week the installer cloned each one at main. The database came from Homebrew at whatever version was stable. Nothing tied the two together.

That gap produced three real failures in September.

  • A node that removes something breaks an app that expects it. The node parses every request strictly and answers an unknown field with a bare 400. An app on main sent a field the installed node did not know. The app saw “400” and nothing else.
  • Our public mirrors lagged unevenly. One app was 37 commits and 16 days behind its source; another was current. A fresh install got a set of commits that no test had seen together.
  • The install path depended on a live service. One quota error on a cloud endpoint turned a fresh install red one weekend. A read path should not be able to fail that way.

The fix was not a minimum-version rule. A minimum says “this app needs LastDB 0.23 or later.” It is silent when 0.24 removes a field. What we wanted was a record of pairs that were actually run: this app commit, with this node build, passed a fresh install. The registry is that record.

What the registry is

The registry is one JSON file per channel, next to a detached signature. Public location: the Homebrew tap repository, served raw by GitHub.

registry/stable.json        registry/stable.json.sig
registry/next.json          registry/next.json.sig
registry/index-signing.pub
registry/proofs/<run-id>.json

Each app in the file has a source URL, a description, and a list of compat rows. A compat row is a proved pair: this app version at this commit was proved with this node build, by this proof run, at this time. Here is the live row for Kanban, exactly as lastdb app info kanban prints it today.

{
  "app_id": "kanban",
  "source": "https://github.com/EdgeVector/fkanban.git",
  "description": "Kanban board over LastDB.",
  "compat": [
    {
      "app_version": "0.1.0",
      "sha": "90ef24ad13f297b48cd9b0da93dd5c57245db6bf",
      "lastdb_version": "0.23.3-2127-gaf73aa569",
      "proved_at": "2026-09-20T22:40:15Z",
      "proof_run": "llms-smoke-20260920T232635Z-0.23.3-2127-gaf73aa569"
    }
  ]
}

proof_run is not decoration. It names a record under registry/proofs/ that says which checks ran and that they passed. The install command checks that the record exists.

THE REGISTRY IS ONE SIGNED FILE stable.json one row = one proved pair APP @ COMMIT NODE BUILD PROOF RUN brain @ 1f2d2f46 0.23.3-2127 llms-smoke-…2635Z kanban @ 90ef24ad 0.23.3-2127 llms-smoke-…2635Z situations @ 923409b1 0.23.3-2127 llms-smoke-…2635Z 9 apps · sha256 of the exact bytes · Ed25519 signature in stable.json.sig HTTPS GET lastdb on your machine VERIFYING KEY, PINNED 1. hash the bytes it received 2. compare with the .sig payload hash 3. verify the Ed25519 signature any mismatch refuses the whole file PROVED ROW PUBLIC HOST, NOT YOURS KEY
Fig. 1 — The registry: one signed file of proved pairs, verified by a key pinned in the lastdb binary

The signature file carries the SHA-256 of the exact index bytes and an Ed25519 signature over that hash. lastdb downloads the file, hashes what it received, compares, then verifies the signature with a key compiled into the binary. Any mismatch refuses the whole file. There is no account step and no token. A test index can use a different key, and the override is printed so it is never silent.

Why a static file

A fresh install is the one moment a user has nothing yet. It should depend on as little as possible: a CDN, a hash, and a signature. The write side of the registry — where a developer publishes an app — still uses a developer certificate and a service. The read side does not. The two were split on purpose.

How install picks a row

lastdb app install <name> asks the running node for its build string (GET /api/version, new in this release). Then it reads the index and picks the newest compat row whose lastdb_version equals that string. Equal, not greater-or-equal.

INSTALL PICKS A ROW BY EQUALITY, NOT BY MINIMUM YOUR NODE build 0.23.3-2127 GET /api/version A ROW NAMES THIS BUILD? YES CHECKOUT newest such row + receipt with proof id NO STOP. NOTHING INSTALLS. the error names the remedy: brew upgrade lastdb a minimum-version rule fails when the node REMOVES a field; an equal pair was actually run.
Fig. 2 — Resolve: the node build string is the lookup key; no equal row means no install

If a row matches, install clones the source, checks out that commit, verifies HEAD, and writes a receipt next to the checkout. Here is a real one from this morning.

$ lastdb app install situations
installed: situations 0.1.0 @ 923409b1a3e9 (proved with lastdb 0.23.3-2127-gaf73aa569 by llms-smoke-20260920T232635Z-0.23.3-2127-gaf73aa569)
  source: https://github.com/EdgeVector/situations.git
  checkout: ~/.lastdb/apps/situations/source

$ cat ~/.lastdb/apps/situations/lastdb-app-install.json
{
  "app_id": "situations",
  "channel": "stable",
  "lastdb_version": "0.23.3-2127-gaf73aa569",
  "proof_run": "llms-smoke-20260920T232635Z-0.23.3-2127-gaf73aa569",
  "sha": "923409b1a3e9e810f5c9b5c8155ae25af4a9f7a5",
  "source": "https://github.com/EdgeVector/situations.git",
  "trust": "pinned",
  "version": "0.1.0"
}

If no row matches, nothing installs. The error names the remedy: brew upgrade lastdb, or wait for the next proved set. We chose the hard stop over a fallback because a fallback is how the September failures happened.

The catch we hit while building it

Our first rehearsal of the whole loop reported GREEN. It was false. The node under test was new, but the apps had quietly fallen back to main under an older lastdb on the path. Every check passed. None of them had exercised a proved pair. The fix was the receipt above: the smoke now refuses to pass unless every installed app carries a receipt whose proof run exists. A green that does not say what it proved is not a green.

Who writes the rows

Nobody types a row by hand. Both channels started empty on purpose. A row is written by an automated job of ours, once a night, and it is written only after the exact pair passed a fresh install.

  1. Build the node from the tip of main.
  2. Fix the commit of every app in the set (nine today).
  3. Install the whole set into an isolated, empty home with that node, and run the public install checks from our own docs against it. 37 checks.
  4. On green, the same job cuts our own daily-driver node over to that build. We run what we are about to publish, on the database that holds our own work.
  5. Write the rows and the proof record to next.

Then the pair set soaks for 24 hours on that node. An hourly job watches it. When the window is clean, one person runs one command, and two things publish together: the node bottle to Homebrew stable, and the rows to stable.json. The first stable publish happened on 2026-09-21: LastDB v0.23.5 and nine rows.

A ROW EXISTS ONLY BECAUSE A FRESH INSTALL OF IT PASSED BUILD NODE from main, nightly FIX APP COMMITS ×9 FRESH INSTALL isolated home, that node, those commits · 37 checks GREEN? YES next.json rows + proof record 24 H CLEAN SOAK ONE HUMAN one command PUBLISH BOTH brew node bottle stable .json FIXED COMMIT / SEALED BYTES CHECK SEALED PARCEL
Fig. 3 — The loop: nightly candidate set, fresh install in isolation, soak, then one human command publishes node and rows together

The stable step stays a human action for now. We will revisit that after five clean manual publishes. The point of the loop is not that a machine presses the button. The point is that the button only exists when the proof exists.

Start using it

You need LastDB 0.23.5 or later. Earlier builds have no app resolve; the installer falls back to main and says so.

brew install edgevector/lastdb/lastdb   # or: brew upgrade lastdb
brew services start lastdb

lastdb app list                              # the shelf, with the row for your node
lastdb app info brain                        # one app's rows
lastdb app install brain                     # one app, at its proved commit
lastdb app install kanban situations         # several at once
lastdb app upgrade brain kanban situations   # move to the newest proved pair

lastdb app list shows every app on the shelf and the row that matches your node.

$ lastdb app list
# stable index · https://raw.githubusercontent.com/EdgeVector/homebrew-lastdb/main/registry/stable.json · node 0.23.3-2127-gaf73aa569
brain        0.8.0 @ 1f2d2f46a7eb  1 rows  Long-lived knowledge: decisions, rationale, references.
kanban       0.1.0 @ 90ef24ad13f2  1 rows  Kanban board over LastDB.
lastsecrets  0.1.0 @ f293954f3e85  1 rows  Local secrets over LastDB.
search       0.2.0 @ 9390c422afcf  1 rows  Local semantic search plane.
situations   0.1.0 @ 923409b1a3e9  1 rows  Shared operational posture and preflight.
…

Install puts the checkout under ~/.lastdb/apps/<app>/source. The dependency step and the link into your path are still yours; the apps page has the two lines for each app. If you use the Last Stack installer, it now calls the same commands for you and refuses to run an app without a receipt.

lastdb app upgrade reinstalls only when the resolved commit differs from the one on disk. Run it after brew upgrade lastdb: a new node build has its own rows, and the apps should move with it.

What it does not do yet

  • No stars, install counts, or recommendations. The shelf is a list of proved pairs, not a store.
  • No automatic publish to stable. A human runs the command, after the soak.
  • Rows are per node build. A build that no job has smoked yet has no rows. That is the design, and it is also why brew upgrade lastdb is the remedy in the error message: the bottle and its rows publish together.
  • Third-party apps publish through the same developer path as ours, but the nightly job only proves the set we ship. A row for an outside app needs its own proof run, and that path is not built yet.

The one idea

A version number is a claim. A proved pair is an observation. The registry stores observations, the install command reads them, and the error message tells you which observation is missing. Everything else is plumbing.

Related: the apps shelf, the install docs, and Prove It To Land on why nothing lands here without a proof of the user-visible capability.

[← Blog]