[← Blog]

Dogfooding the App Registry

The first app we tried to put in the LastDB app registry could not get in. Registering a novel schema required the app to exist first. Publishing the app required its schemas to not be novel. Two rules, each sensible alone, together a door locked from both sides: an app with any novel schema could never enter the registry at all. We found the deadlock the intended way — the registry starts empty, so somebody had to be first through the real path, with a fresh signing key and an app the catalog had never seen. Below: the fix, the other four defects the walk surfaced, and the walk replayed on the fixed path, ending at tier live.

Two rules, one circle

The register side, as captured on the first attempt:

error: schema 'PantryItem': registration failed: … 403 Forbidden: {"reason":"app_not_registered"}

The service refuses to register a schema for an app it has never heard of. Reasonable. The publish side refused with the mirror instruction: novel schemas present, register them first. Also reasonable. Together they closed the circle: an app whose schemas were genuinely new could not become an app, and its schemas could not stop being new until it did. Each error was individually correct; the sequencing was wrong. No error message can talk you out of a deadlock.

Publish reserves, promote gates

The fix used a distinction the registry already had: tiers. Publish now does one small thing: it reserves your app id, and the row lands in a sandbox tier (reachable by id, off the default shelf), so schema registration has an app to attach to. The hard no-novel-schemas rule moved to promote, the step that puts a row on the visible shelf. The invariant — nothing unregistered is ever visible — is preserved. The circle is gone.

1 · CHECK catalog verdicts 2 · PUBLISH reserve · sandbox 3 · REGISTER-SCHEMAS stored identity → lockfile 4 · PROMOTE any novel shape? YES → rejected, register it first NO → live THE REGISTRY tier: live PROCESS GATE STORE REJECT PATH POCHÉ = REAL ROWS
Fig. 1 — the shipped flow: publish reserves a sandbox row; the gate sits at promote, not publish

The walk, replayed

The test subject: pantry, a small app that tracks what’s in your kitchen, with two schemas the shared catalog genuinely didn’t know. Setup is one command; lastdb app dev-init generates the key that signs everything you publish. Here is the walk as it runs today, fixes landed, including our own attempts to skip ahead. First, ask what the catalog already covers:

$ lastdb app check --manifest pantry.json
app: pantry
  PantryItem   NOVEL  not covered by the shared catalog [catalog-confirmed novel]
  RestockRule  NOVEL  not covered by the shared catalog [catalog-confirmed novel]
note: 'RestockRule' is novel and will need field_descriptions to register
      — missing: item_name, min_quantity, reorder_amount, preferred_store

Two details in that output exist because this run first failed without them: the catalog-confirmed verdicts and the named missing fields. Both are in the defect list at the end. Reserve the name, and try to skip ahead:

$ lastdb app publish --manifest pantry.json --env dev
published (created): {
  "app_id": "pantry",
  "code_signature": null,
  "env": "dev",
  …

$ lastdb app list --env dev
pantry	Sandbox	Pantry	Tracks what's in your kitchen: items, quantities, expiry dates, and restock rules.

$ lastdb app promote --manifest pantry.json --env dev
error: promote rejected: novel schemas present (PantryItem, RestockRule) — register them first with `lastdb app register-schemas`

The name is reserved, the row is listed at tier Sandbox, and the gate holds until the shapes are registered. Registering fails too, client-side and before any network call, because the manifest still lacks those four field descriptions:

$ lastdb app register-schemas --manifest pantry.json --env dev
error: schema 'RestockRule': novel registration requires a field_descriptions entry for every field — missing: item_name, min_quantity, reorder_amount, preferred_store. Add them to the manifest and re-run.

Add the descriptions and register again:

$ lastdb app register-schemas --manifest pantry.json --env dev
registered: PantryItem → catalog identity 76ce8941… (folded into an expanded canonical; local shape was a4a49710…)
registered: RestockRule → catalog identity 76ce8941… (folded into an expanded canonical; local shape was a73d8323…)
lockfile updated: pantry.json.lock.json
all manifest schemas are catalog identities — ready to publish
(note: `lastdb app check` may keep reporting them novel until the next resolver pack propagates)

One subtlety: both proposals came back with the same catalog identity. Rather than mint duplicates, the catalog may fold your proposal into a shape it already knows, so the identity it stores is not always the shape you computed locally. That is what the lockfile is for: a freshly stored identity is not yet resolvable from the node’s own view, and on an earlier run every verification read reported the schemas missing seconds after the catalog accepted them. The CLI now records the identity the catalog actually stored and verifies that exact identity from then on. The trailing note is the same lag stated plainly: the node’s local view can trail the catalog’s answer for a while. Promote now passes the gate:

$ lastdb app promote --manifest pantry.json --env dev
promoted: {
  …
  "registered_at": "2026-07-17T15:49:21Z",
  "tier": "live",
  "uses": []
}

That is the whole developer surface: check → publish → register-schemas → promote, and the app sits on the shelf next to the apps we build LastDB with.

What else the walk shook loose

  • Registration wrote where no reader looked. Early on, two successful registered: lines were followed by “error: schemas still not in the catalog after registration: PantryItem, RestockRule” from the very next command. The write landed in a location no read surface checked; registration now writes entries every read surface agrees on.
  • A stale local view reported as novel. A fresh node called every schema novel because its own copy of the catalog had not caught up. check now confirms novelty with the catalog itself before saying the word; that is the catalog-confirmed in the first transcript.
  • Field descriptions only surfaced server-side. They were mandatory for novel schemas, and the only way to learn that was a rejection three steps into the flow. The refusal is now immediate and local, as in the transcript above, and check names the same gaps before you ever register.
  • The promote gate trusted the local view. The gate now re-checks the catalog’s own answer before deciding, and when it cannot reach the catalog at all it fails closed: schemas the node considers novel stay novel, and the promote is refused.

What we didn’t do

When the first app couldn’t get in, the shortcut was obvious: insert the row by hand and move on. We fixed the registration path instead, and each fix above (the tier split, catalog-confirmed checks, client-side manifest validation, the lockfile, the fail-closed gate) now applies to every registration that follows. The premature-promote rejection in the transcript is asserted on every run of an automated dogfood rotation; a regression there fails the rotation. The pantry row is on the shelf, and it came through the same path everyone else will use.

Companion piece: How an App Gets Published on LastDB for the design; Declared, Not Registered for how schema resolution works under it.

[← Blog]