Plebeian · architecture review · v1 · 19 Sep 2026

One platform.
Many modules.
No landlord.

A modularization plan for Plebeian — the market, the CMS, the meeting place and the makers' agent. Built on TypeScript packages and web components, distributed over Nostr and Blossom, forked and extended with agent assistance. This page is the top-level map; the technicalities are on purpose.

market world agent
615files · ~147k lines in src/ today
19ADRs, with a numbering collision to fix
6module spec sheets drafted
5 / 23napplet domains actually merged
The mission, in one breath

Value stays where it's made.

Three apps on one sovereign stack, each self-hosted on your own box under your own keys: Plebeian — the market and the circular-economic CMS; Torii Quest — the meeting place in 3D; Continuum — the makers' agent with encrypted memory. GPL-3.0. Non-custodial. Nostr-native. Lightning and Cashu. Farmers' markets as the beachhead, community co-ops as the shape.

The market

Plebeian

Publish your site, list goods, run auctions, chat with buyers. Visual site and store builder, NIP-99 marketplace, onchain · Lightning · Cashu · zaps, neighbourhood directory and pickup listings.

The meeting place

Torii Quest

Browser-native 3D, mobile-first PWA, portable characters, gateway hops between community worlds. A market square that never closes.

The makers' helper

Continuum

Personal AI on your box or self-routed. Encrypted memory keyed to you. Scheduled agents, long-running tasks. It runs the site, greets buyers, and writes new corners of it while you sleep.

What we covered

The decision ledger.

Every call below was made in this review, with the evidence behind it. Nothing here is a promise — each line is a rule the code will obey.

settled

The base is packages, not applets

TypeScript packages plus a reusable web-component library, with one spec sheet per module. Napplets are a projection, not the foundation — their draft NIP is unmerged and their dependency model inlines every package into every artifact.

settled

Modules are the abstraction

A module has a spec, a pure core, bindings and components. How it runs — in-process, sandboxed, agent-authored — is a property of the module, not a different architecture.

settled

The trust boundary moves, it doesn't vanish

Shared CMS modules are declared dependencies reviewed by the operator, their agent, or a third-party service. Auth and wallet stay behind a strict boundary that module code cannot import.

settled

Forking is the feature

The point of self-hosting is that you can fork the code, the theme, the CMS and the modules — with Continuum doing the rewriting. That makes spec sheets the agent's input format, not documentation.

Modularization of the current platform

From one app to a graph of modules.

Plebeian today is a working monolith: routes, components, stores, query and publish layers, all wired through a global signer and relay pool. It already contains the seams of the modules — they're just not separated. Modularization is the act of naming them, giving each a spec, and cutting the wires that shouldn't exist.

today · the monolith

One process, shared everything

routescomponentsstoresqueries / publishglobal signer
  • 615 files, ~147,000 lines in src/, plus ~15,000 of e2e
  • The cart store alone is 1,911 lines and mixes state, queries, persistence and shipping matching
  • Validation schemas exist but are imported by nothing on the browse path — reads cast raw tags
  • 19 ADRs, with two different ADR-0006, a gap at 0017, and one unnumbered
  • Every surface can reach every other surface, so changes ripple
target · the graph

Modules with declared edges

speccore packagebindingscomponentsCMS page
  • A module owns one job and publishes one interface
  • Pure logic lives in a package with no ambient authority — no fetch, no storage, no signer
  • Anything privileged arrives as an injected capability, never as an import
  • State is either a published event (durable, portable) or scoped ephemeral cache
  • Cross-module talk is a contract: an event, or an intent — not a shared store handle
anatomy of a module · the ten sections every spec sheet carries

What a module actually is

  • Identity — one job, owner, maturity, spec version
  • Spec sources — which NIP or spec it implements, and what we define ourselves
  • Data contract — kinds, tags, validation, and the gating rule per surface
  • Capability interface — what it needs from its host, each mapped or marked runtime-only
  • State ownership — what survives a rebuild, and what must never be persisted
  • Platform projections — in-process, sandboxed, mobile, self-hosted
  • Composition — which components and artifacts it ships
  • Permissions & consent — grants, rebuild impact, fund-safety rules
  • Tests & conformance — unit, conformance, e2e, cross-adapter invariants
  • Versioning — spec version vs package version vs build hash
// the interface a module declares — capability names, never imports
interface ModuleHost {
  events: { query(f: Filter): Promise<Event[]>; publish(t: Template): Promise<Event> }
  identity: { current(): Pubkey }
  storage?: { get(k: string): string | null; set(k: string, v: string): void }
  payments?: { request(i: Invoice): Promise<PayReq>; status(id: string): Promise<Settled> }
  theme?: TokenMap
}
// no signer. no wallet URI. no network. those are host-owned, always.
Spec sheet Core package — pure Bindings — one per runtime Components — token themed Page slot — in the CMS 01 · one job, one interface 02 · no fetch, no storage, no signer 03 · in-process today, sandboxed later 04 · props in, tokens applied 05 · declared, hashed, reviewed capabilities inevents · identity · theme · payments blocked: signer · wallet · keys · order decryption
a module, top to bottom
Farmer Neighbour Maker Community pot sells the harvestpays direct · no processor builds & shipsopt-in share back the split you choose it same npub · same circle · any node
the circle — one module, its own spec
module 01–02

Cart & browsing

Cart: kind 30078, d=plebeian-market-cart, version 1. The cart of record is a published event, never sandbox storage — so a rebuild can't eat a buyer's basket. Browsing: read-only, requests no write capability at all.

module 03–04

Checkout & orders

Checkout splits three ways: wizard and address form applet-side; invoice generation, paying, and NIP-17 wrapping host-side. Orders read and render anywhere; the transport is privileged.

module 05 · promoted

CMS & the circle

The CMS is the headline feature, not a side quest: visual site builder, themes, published sites as content-addressed nsites. The profit-share circle becomes its own module — the split at payment, the statements, the pot.

Interconnection · not isolation

Nodes that trade with nodes.

A citadel is not an island. Every node is a relay plus a gateway plus a blob store, and the links between them are protocols, not integrations. Identity travels, content travels, orders travel — the software stays where it is.

nostr relay ↔ relay · outbox gossip blossom blobs · content-addressed, shared torii gateway hop Citadel A relay + market sniper · torii Citadel B farmers' market co-op keys Torii Quest world portable characters same npub, same bag One identity npub · cart · orders · reviews · character
link 01

Nostr relays — Sniper

Each node runs its own relay; nodes follow each other. A seller's listing on Citadel B is readable from Citadel A because the buyer's client queries both. The relay is the node's public presence — and its git-over-nostr home for the forkable source.

link 02

Blossom — the shared blobs

Images, themes, world assets, published sites and module bundles are content-addressed and served by any node. Same hash, same bytes, anywhere. A fork doesn't need a re-upload; it needs the hash.

link 03

Torii gateways

The node's public face: domain, TLS, deploys, and the hops that carry a character between community worlds. Gateways are how two circles meet without merging their infrastructure.

Role mapping to confirm: Sniper = the relay/node layer, Torii = the gateway and suite host layer. If the split is different in your head, say so — the diagram is one line to redraw.

CMS modules vs napplets

The same abstraction, two runtime kinds.

Your framing is the one I'd adopt: a shared CMS module is a declared dependency, reviewed before install — by the operator, their agent, or a third-party service. Napplets remain supported through that exact system, as a stricter runtime kind, not as the main system.

dimension
CMS module — the main system
Napplet — the strict kind
Trust model
Declared dependencies + review gate. Runs with the node's authority.
Sandbox: no keys, no network, no storage beyond a scoped store. Enforced by the browser.
Cost to build
Low: a package, a manifest, a page slot.
High: a second runtime, a shell, window management, per-artifact consent.
Reuse
Imported once, shared by every module that needs it.
Inlined per artifact — every napplet carries its own copy of every package.
Identity & updates
Version the package; the page keeps its address.
The build hash is the identity: any byte change re-consents and wipes scoped storage.
What it cannot do
Cannot be safely run from an untrusted author without review.
Cannot sign, cannot reach a relay, cannot read a wallet — by design.
Where it belongs
Everything: the market, the CMS, the themes, the modules, the forks.
Third-party UI you will not review. Rare, and optional.
the strict boundary that stays

Auth and wallet never become importable

A module gets a facade, not the module graph. The signer, the wallet, the NWC connection and the private keys live behind a host-owned capability that requires an explicit, visible approval per action. That is what keeps "we never touch your money" true even with third-party code installed.

// module manifest — the declared surface is the reviewed surface
{ "name": "stall-gallery", "version": "1.2.0",
  "hash": "sha256:9f2c…", "reviewed_by": "node-operator",
  "requires": ["events.read", "theme.tokens", "media.read"],
  "never": ["signer", "wallet", "orders.decrypt"],
  "slots": ["storefront.after-hero", "cms.page.section"] }
Forking, with the agent in the loop

Customize it like it's yours — because it is.

This is the part that makes the architecture pay off. Self-hosting is only freedom if changing the thing is cheap, and that's what Continuum is for: it reads a module's spec, rewrites the module, and proves the rewrite still holds.

the loop

Spec → change → verify → publish

spec sheetagent editconformancediff reviewblossom + nsite
  • The spec is the agent's input format — not documentation for humans only
  • Invariants and tests are the acceptance gate: the change must still pass
  • The operator reviews the diff, or delegates that review to their own agent
  • The fork is published content-addressed, so other nodes can install it by hash
the boundary, again

What the agent may touch

  • Yes: themes and tokens, page layouts, CMS sections, module config, component variants, copy, pricing rules
  • Yes: new modules built against an existing spec, and new slots in a page
  • Never: the signer, the wallet, NWC connections, private keys, escrow rules
  • Never: silently widening a module's declared capabilities
  • Always: a visible, per-action approval when money would move
Sequence

What gets built, in order.

step 1

Fix the ground

Resolve the ADR numbering collision, freeze the spec-sheet template, and adopt the capability interface as the house rule.

step 2

Cart & browsing packages

Extract the pure cores, write the in-process bindings, and make the components take props instead of reading a global store. First real proof of the module shape.

step 3

The CMS and the theme contract

Visual site builder, token-based theming with a three-colour floor, and published sites as content-addressed nsites over Blossom.

step 4

The circle

Profit-share as its own module: the split at payment, the statements, the community pot, the co-op configuration.

step 5

Checkout & orders

Define the payments capability — receive, status, balance — and keep paying and wrapping host-side. Close the order-flow defects found in review.

step 6

Module manifest & review gate

Declared capabilities, content hashes, page slots, and the review path: operator, their agent, or a third-party service.

step 7 · optional

The strict runtime kind

Add the sandboxed projection as one more adapter, for the day a third party ships UI nobody wants to review. The interface is already there.

always

Honesty in the UI

Loading states that admit they're loading, validation on the read path, and named failures instead of silently shorter baskets.

The shape of it

Same freedom, less scaffolding.

The napplet model asks you to rebuild an operating system before you can ship a button. The module model asks you to write down what each piece needs — and then lets the pieces be shared, forked, reviewed and rewritten. Keep the capability interface, keep the content addressing, keep the strict boundary around keys and money. Drop the second runtime until someone actually needs it.