
← Open the market app — this page is the documentation; the app is the product.
This page is market-app/visuals/webui/index.html, served by
visuals/webui/run.ts. It declares the shell's three regions and renders the homepage rows the app
composed against the browse and auctions reads, over the relay policy the app injects. It
implements no feature rule of its own: if a row needed a rule, the rule already lives in the module that owns it.
The app is the composition root made visible: the one node that merges the buying track and the money
layer, turned into a page. It lays out three regions, declares a route table, supplies the render port
each surface draws through and composes the homepage rows. It is composition, not a
feature: the product rows come from browse's listing read and the auction rows from
auctions' listing read, and the shell restates neither.
The relay set is injected policy: the app holds it (the staging app relay plus the read pool) and hands it to the reads through the seam it supplies. No feature module fixes an address and the page reaches no remote asset — the one script asks this page's own run program, on this origin, for the rows it already read.
Below is the shell itself: the nav bar at the top, the content area (the homepage rows) and the cart corner beside it.
The nav bar holds one entry per module a build composes: the app's own static entries, and a plugin each module contributes for its own panel (Cart, Wallet, Signer, Nostr settings). No plugin is composed in this build yet, so the bar shows the app's static entries only — an absent plugin contributes no entry and breaks nothing.




















































































































































































Cart · empty
A persistent affordance on the page (a menu entry with a count badge on a phone) that opens the cart's own panel. It reads the cart's projection; it owns no cart state. The cart module's panel is not composed yet, so the corner reads empty.
The regions above are the page's own layout. The content region shows the real homepage rows the run program composed — not a mock-up.
Each row is its own grid with its own filter — one grid per row rather than one grid with a single filter — so a row the person is looking at filters alone and the other rows do not move. That is the market's own homepage shape (a grid per featured section) and the only shape in which a "latest" row and a category row can sit side by side with independent filters.
A row's header states its filter. The latest products and latest auctions rows carry a search box; each category row carries a category selector you can change. Changing a row's filter re-renders that row's grid in place — no reload — and the row's count reads showing N of M. The filter is applied to the listings the row already holds; it opens no socket.
The bar is not a list of links the app hard-codes. Each module that owns a thing declares how you
configure it, by implementing the shape the UI contract declares — NavPlugin:
interface NavPlugin<VM extends SurfaceViewModel = SurfaceViewModel> {
readonly id: string // stable and unique
readonly label: string // the nav entry's text
readonly order: number // its place in the bar
readonly surface: Surface<VM> // the panel it contributes — a surface
}
handed: the render port never: a rail never: custody never: key material never: the app store
The app admits the plugins a build composes, orders them by each one's declared order and
mounts each through the render port — and it admits only well-formed plugins: a malformed one is refused
by name, and an absent one leaves the bar standing. The app declares the slots; the modules fill them.
The app alone owns the route table, so a module renders and the app binds. Each row names one path and the surface served there:
/ → home.rows — the homepage (the app's own composition; shown above)./products → browse.listings; /products/:ref → browse.detail./cart → cart.intent; /orders → orders.machine; /orders/:id → orders.detail./checkout → checkout.flow; /auctions → auctions.surface; /nostr → nostr.settings.A surface a later increment has not yet shipped leaves its route's region in its empty state, named, not blank.
plebctl webui run market-app app), on port 4336. The run program reads the
relays over the wire; the page itself loads no remote asset and its one script calls only this origin's
/api/home.It owns the layout regions, the route table, the render port, the registry that binds every declared port, the resources a module may not reach for (custody, the engine, the stores and the clock) and the relay policy it injects. It merely hosts every feature surface — it draws none of them, reaches inside none of them, and states no cart rule, no order state, no flow step, no bid precedence, no product or price field and no funds rule of its own. The homepage rows are composition: it names which module's read fills which row and in what order.