{
  "schema": "leumas.docs.page/1",
  "id": "how-to:build-an-item-type",
  "slug": "how-to/build-an-item-type",
  "kind": "pages",
  "bucket": "how-to",
  "title": "Build-knowledge — item types: what the App Store can sell, and where a purchase lands",
  "name": "item types",
  "eyebrow": "build knowledge",
  "chip": null,
  "summary": "The store used to sell one kind of thing. Not by design — by five files each hardcoding the answer, and a sixth question nobody had asked.",
  "keywords": [
    "build-an-item-type",
    "hardcoding",
    "sell",
    "record controls",
    "question",
    "leumas build an item type",
    "five",
    "design"
  ],
  "audience": "both",
  "funnel": {
    "product": null,
    "cta": null
  },
  "body": "# Build-knowledge — item types: what the App Store can sell, and where a purchase lands\n\nThe store used to sell one kind of thing. Not by design — by five files each hardcoding the answer,\nand a sixth question nobody had asked.\n\n## The one idea\n\n> **An item type is one record that answers \"what is this?\" and \"where does a buyer's copy go?\".**\n\nThirteen of them live in `shared/packages/item-types/src/types/<id>/meta.js`. Everything else —\nthe wizard's picker, the scaffold, the agent's toolset, the preview, the packer, the install lane,\nthe Purchased tab's link — reads that record. Adding a fourteenth kind of sellable thing is a\nfolder and one line in `registry.js`.\n\n## The package is split in half, and the split is load-bearing\n\n| entry | holds | who imports it |\n|---|---|---|\n| `@leumas/item-types` | `define.js`, `families.js`, `registry.js`, the 15 metas — **no React** | leumas-api, @leumas/workspace, @leumas/coding-agent, @leumas/marketplace, @leumas/plugin-host |\n| `@leumas/item-types/catalog` | the 8 structural preview renderers, behind `lazy()` | Studio only |\n\nThe API is the heaviest consumer (project kinds, scaffolds, packers, install lanes) and has no DOM.\nOne `import { lazy } from 'react'` reachable from the main entry and leumas-api fails to boot, so\n`check:item-types` fails the build on a server file importing `/catalog`, and on any React import in\nthe eager half. The JSX half of that rule is proven by the guard *importing* the registry through\nbare Node — a JSX tag anywhere would be a SyntaxError before the guard reached its own assertions.\n\nIt also keeps the picker cheap: the wizard draws fifteen tiles from fifteen ~40-line objects and\nfetches no preview chunk until somebody actually builds something. Same trade `@leumas/inputs` makes.\n\n## The five families — thirteen types, five arrival lanes\n\nTypes that arrive the same way share their install code. That is what makes thirteen tractable.\n\n| family | types | where a purchase lands | executes? |\n|---|---|---|---|\n| **sandbox** | `app`, `os-app` | `/admin/p/:appId` iframe · OS desktop | yes |\n| **dynamic** | `server`, `router`, `script`, `schema`, `component`, `input` | Dynamic layer · LMX · form CONTROLS | yes — **arrives OFF** |\n| **document** | `workflow`, `grid` | Workflows · Grids | no (interpreted) |\n| **machine** | `mcp-server` | the MCP gateway | no |\n| **asset** | `media`, `digital-product` | Files › Library, downloaded | no |\n\n`circuit` and `arduino` are also registered, `sellable: false`. They predate the store and are not\nsold; they exist here because `PROJECT_KINDS` and the agent's toolset map are now DERIVED from this\nregistry, and dropping them would strip Circuit Studio and the Arduino IDE of their quota lanes.\n\n## Three rules that are security, not tidiness\n\n1. **The lane comes from the STORED listing's `itemType`, never from a submission.** `upsert` spreads\n   `req.body` wholesale and `POST /listings` accepts it from any holder of `distribute_mcp`, so\n   `clampItemType` runs on the way IN and again in `publicListing` on the way OUT — twice, because a\n   defence that only holds for rows written through one code path is not a defence. An unrecognised\n   value becomes `app`. The `delivered` block is DERIVED, never stored: a listing that could describe\n   its own arrival would be describing where it gets to install itself.\n2. **`enabledOnArrival` is a property of the FAMILY, not the type.** A per-type flag is a per-type\n   chance to hand a stranger a running process. Anything in `dynamic` arrives switched off; the buyer\n   reads what it declares and enables it.\n3. **`install.collection` is an allow-list per family.** `defineItemType` refuses a destination the\n   family does not name, because a successful write into the wrong table is indistinguishable from a\n   correct one at every layer above it.\n\nNothing in the delivery path ever executes what it delivers. Reading a packed document is\n`JSON.parse`; running it is the destination engine's job, later, under the buyer's own gates.\n\n## What one record controls\n\n```js\ndefineItemType({\n  id, label, blurb, icon, family, category, tags,\n  projectKind,          // defaults to `id` — so project.kind, the agent's mode and the item type\n                        // are ONE value with nothing to keep in step\n  scaffold,             // a folder under factory/boilerplates\n  entry,                // what the preview, the packer and `validate` all look for\n  quotaCap,             // a dotted entitlement key, or absent = UNMETERED (never invent one)\n  preview,              // a host renderer ('component'|'html') or a package one\n  wizard,               // extra FormWizard steps, as DATA\n  agent: { instructions, toolset },\n  validate,             // (files) => { ok, problems[] }\n  pack: { strategy, exts },\n  install: { collection, path, label },\n  pricing: { defaultModel, meterTarget },\n});\n```\n\n**Previews are keyed by RENDERER, not by type.** Four types (`app`, `os-app`, `component`, `input`)\npreview by compiling JSX, and compiling is host-owned: `compileDynamicComponent` runs `new Function`\nsame-origin with the operator's session, so the decision about whose code goes through it lives in\nONE place next to the sandboxed-iframe fallback it is weighed against. The eight package-owned\nrenderers are pure functions of a document — they parse and draw, never evaluate — which is what\nmakes them safe to point at something you have not decided to trust.\n\n## The five seams this replaced\n\n| was | now |\n|---|---|\n| `CreatePluginWizard`'s 4 cosmetic kinds, *\"not stored\"* | the wizard picks a real type and **stores it** |\n| `PROJECT_KINDS = ['app','circuit','arduino']` | `projectKinds()` |\n| `QUOTA_CAP = {…}` | `quotaCaps()` — provably the same three entries |\n| `if (mode === 'circuit') / if (mode === 'arduino')` | one lookup over an injected `toolsetFactories` map |\n| `packBundle` always making an iframe bundle | one packer parameterised by `exts` + `shell` |\n| — | `createDeliverer` — the sixth seam, which did not exist |\n\n`packBundle` was **parameterised, not forked**. Everything that makes it safe is the same for every\nkind of thing: the explicit-stack walk that refuses to follow a symlink out of the sandbox, the size\nlimits checked while reading, and the rule that `leumas.app.json` is generated from the stored listing\nrather than copied from a file the publisher controls. Three packers would have been three copies of\nall of that, and a security rule in three copies is one that will exist in two.\n\n## Adding a type\n\n1. `src/types/<id>/meta.js` — `defineItemType({...})`.\n2. One import + one `ENTRIES` line in `registry.js`.\n3. A `factory/boilerplates/<scaffold>/` folder containing the `entry` file.\n4. A renderer in `catalog.js`'s `RENDERERS` if it needs a new one.\n5. `pnpm check:item-types`.\n\nThat guard asserts folder<->registry parity, a React-free eager half, no server file importing\n`/catalog`, renderer parity in both directions, a default export on every renderer, no literal colour\nin one, that every `install.path` is a route the nav manifest or route table actually declares, and\nthat **every scaffold folder and entry file exists** — a missing boilerplate is otherwise silent,\nbecause `scaffoldInto` treats an unknown template as \"write the blank starter\" rather than an error.\n\n## Guards\n\n`pnpm check:item-types` · `smoke:store` (58) · `smoke:apps` (608) · `smoke:app-catalog` (41) ·\n`smoke:plugin-proxy` (41) · `check:sandbox` · `check:nav` · `check:huds` · `check:admin`.\n",
  "source": {
    "path": "shared/services/knowledge/build-knowledge/build-an-item-type.md",
    "blobSha": "",
    "commit": "",
    "committedAt": "",
    "provenance": "no-git",
    "bytes": 7723,
    "hash": "f5e2e0d9f1d64f4b7142e58dfa68392417a6502d"
  },
  "urls": {
    "html": "/p/how-to/build-an-item-type",
    "json": "/docs/how-to/build-an-item-type.json",
    "md": "/docs/how-to/build-an-item-type.md"
  },
  "links": {
    "composes": [],
    "usedBy": [],
    "product": [],
    "howTo": [],
    "skills": []
  }
}
