The app concept: what a reader sees, and where the index lives

"systematically (default) would appreciate side menu rather than bottom nodes… we prefer index on side… and yes, there will be exceptions… try to work through app concept in general." — maintainer, 2026-09-17

The immediate change (the index moves to the side rail, the bottom catalog goes) is simple. Applying it exposed something larger: an app has no index at all, and the contents catalogs scattered through this repo's plugin pages are the workaround. This page states the concept, names where the current surfaces disagree, and recommends the one rule that would make them agree.

What an app IS today

An app is a tile whose App.Plugin names the path of a node — "usually the Store plugin cover (e.g. Chess, LinkedIn)". The record lives at {user}/_App/{appId} as an ordinary mesh node; the grid is a single-partition query over {owner}/_App, and the tile's name and icon resolve from the app's root node. Default apps are materialized write-behind from Admin/HomeConfig.DefaultApps on home render.

So: an app is a node you open. There is no "app shell", no app-level container type, and nothing that says "these pages belong to this app" beyond the path prefix.

Three navigation models, and where they stop

Surface Who supplies it What it answers
The launcher — home Apps grid, icon tiles grouped by category InstalledApp records + Admin/HomeConfig.DefaultApps Which apps do I have?
A markdown tree — the left-hand rail DefaultNodeNavigation, rooted at the page's SPACE Where am I in this document?
Inside a typed app — a Store/Plugin root, a mailbox, a CRM client nothing

The third row is the gap, and it is not cosmetic:

The measured consequence. 78 node bodies in this repo end in a contents catalog. 54 of them are Store/Plugin roots — app front pages — and their catalog is the only child navigation they have. Removing it would leave those apps with no way in. That is why the 2026-09-17 sweep touched only the 24 Markdown bodies (21 changed, 3 kept as genuine registers) and left the plugin roots alone. The catalogs are a symptom; the missing rail is the defect.

Where the conventions disagree

  1. Two orderings. The rail orders by MeshNode.Order, then name, recursively. The launcher orders by App.Order with 0 meaning "never placed" (painting behind explicitly ordered tiles, most-recently-used first) and groups by category. A reader moving from grid to rail meets two different ideas of sequence.
  2. Two ideas of "the root". DefaultNodeNavigation.IndexRoot is the first path segment — deliberately the space, so one index serves the whole tree. But an app installed into a user home lives at {user}/…, so its index root resolves to the user's home, not the app. An app's index would show the home's tree with the app as one entry inside it.
  3. Embeds get no rail, correctlyshowHeader=false suppresses it and providers are not even asked. Worth keeping, and worth stating: an @@ embed is content, never a page.
  4. The user home is the one typed page that solved this, by embedding @@("area/Catalog") — the Apps grid. That is the correct exception (the tiles are the content), and it is also evidence that the typed-page answer has so far been "hand-roll a region in the body".

The recommendation

One rule: the index is a property of the NODE, not of the markdown renderer.

  1. Give typed nodes the same default rail. Have AddDefaultLayoutAreas() wrap Overview with the navigation MarkdownOverviewLayoutArea already builds — the same SuppliedNavigationRail, the same supplied-then-default precedence, the same two guards (emit at once; a faulted query renders no index rather than holding the page). A Store/Plugin, a mailbox and a CRM client then each get an index for free.
  2. Resolve the index root to the nearest APP ROOT, not the first path segment. An app root is a node that owns a partition, or one an InstalledApp record points at. Chess keeps today's behaviour; a course installed at {user}/AgenticEngineering starts its index at the course instead of the reader's whole home. This is the change that makes rule 1 useful rather than noisy.
  3. Keep INodeNavigationProvider as the override, unchanged. A module that knows its own semantics (a course lists the whole course, not the branch you stand in) still wins over the default. The default exists so that not implementing the seam is no longer the same as having no navigation.
  4. Then retire the 54 catalogs in plugin bodies, leaving the genuine registers. Not before: the sweep is only safe once the rail is there, which is the ordering this page exists to record.

The author-facing rule, once that lands: would a reader look for this in the side rail? Then it belongs in the rail. Embed a catalog only where the listing IS the content — a log, a register, a store page, or a filtered view the rail cannot express (?groupBy=, ?subtree=true). Give it a heading that says what it is (## Correspondence, ## Releases), never a bare ## Contents.

Follow-ups

# Item Why it is not done here
1 Typed nodes render the default rail (AddDefaultLayoutAreas) Core change touching every typed page in the fleet; needs visual verification, which this repo has no local path for (the portal ships in the image)
2 IndexRoot → nearest app/partition root Changes the index of every nested page; wants tests over the installed-course and user-home shapes before it ships
3 Sweep the 54 Store/Plugin catalogs Blocked on 1 — removing them today strips an app's only navigation
4 Reconcile the two orderings (rail Order+name vs launcher App.Order+category) Design question: does an app declare its own section order, and does the rail honour categories?

What changed on 2026-09-17

Reconnecting…
The server was updated. Reloading the page to pick up the latest version.