Docs
/
what you can do

App Store

<!-- GENERATED by ops/tools/skill-sync — do not edit by hand. Run pnpm skills:sync. -->

<!-- GENERATED by ops/tools/skill-sync — do not edit by hand. Run pnpm skills:sync. -->

App Store — what you can do

/admin/d/apps · pillar apps

Browse and install apps, see what you are running, and publish and monetize your own.

Fetch this, never remember it

These change per install and per release. Call for them - a list written here would be a future lie.

WhatCallWhy
what is in the storeGET /marketplace/listingsthe catalog is per-deployment and paginated; a remembered list is wrong the day after it is written
what this tenant has installed, and which domains that unlocksGET /marketplace/installedinstalls are resolved server-side and are per-user as well as per-tenant
what a call would cost before making itGET /passnode/quoteit answers with the gate that WOULD apply and charges nothing - the only honest way to quote a price
the gate types this install offersGET /passnode/typescustom types can be registered at runtime, and one may replace a built-in
a publisher's earnings and what is withdrawable todayGET /marketplace/mine/payoutsthe withdrawable figure is DERIVED from the ledger every time; nothing stores a balance

How to actually do things here

Written by hand, because the order and the traps are the part no file can derive.

Choose the kind before writing anything

  1. Answer the arrival question first: does the buyer OPEN it, RUN it, FILL IT IN, CALL it from an agent, or DOWNLOAD it? That picks the family, and the family picks the shortlist.
  2. Read the table above. Two kinds are usually plausible and only one is right — a component is part of a page, an app IS the page; a script has no branches, a workflow does; an input collects one value, a component renders a panel.
  3. Scaffold from the kind rather than from an empty folder — every kind names its own boilerplate and its own entry file, and the generated one already packs.
  4. If it is game, circuit or arduino, stop planning a listing: those are buildable project kinds and are not sellable today.

Trap: THE FAMILY IS THE SECURITY AXIS, not the type — that is deliberate, because a per-type flag would be a per-type opportunity to get it wrong. Only the DYNAMIC family arrives switched off, because it is the one lane where a stranger's code would run server-side; a sandboxed app arrives on because the browser is what confines it. Each family also has an ALLOW-LIST of the collections it may install into, so a kind cannot nominate a table its family was never meant to touch.

Install something and find where it went

  1. GET /marketplace/listings to find it, then POST /marketplace/install/:appId.
  2. Read the listing's permissions BEFORE installing — the install records exactly what was granted, and that record, not the listing, is what the app runs on later.
  3. Where it lands depends on the kind, and the table above names the destination for each. GET /marketplace/purchased answers it for a specific purchase.
  4. A kind in the dynamic family arrives switched OFF: find it in the Dynamic layer surface the table names and turn it on deliberately.
  5. POST /marketplace/installed/:appId/enabled toggles one without uninstalling; POST /marketplace/uninstall/:appId removes it.

Trap: A purchase does not always become an app in a sidebar — the store sells many kinds and they land in several places, which is exactly why the Purchased tab exists. A core Studio domain has no listing at all and cannot be installed or uninstalled; a request to uninstall one is refused.

Build and publish a kind that is NOT an app

  1. Scaffold from the kind. Every kind names its own boilerplate folder and its own ENTRY file, and the table above prints both — router.js, main.lmx, schema.json, workflow.json, server.json + tools/*.js, and so on. Creating the project posts the kind and the boilerplate together, so the entry file exists before you write a line.
  2. Write the entry file and nothing else structural. The entry is the ONE file the packer, the preview, the readiness check and the installer all read first; a kind whose entry is empty or misnamed installs cleanly and does nothing.
  3. Watch the readiness list in the builder. Each kind carries its own check, and they are specific rather than generic — an MCP server with zero tools is flagged, a schema with an untitled field is flagged, because at every layer above the loader those are indistinguishable from working.
  4. Keep every file inside the extension allow-list the KIND declares. The packer takes the allow-list FROM THE KIND, so a .py beside a router or a .svg inside a media pack is dropped with a warning at submit, not refused — the bundle simply ships without it.
  5. Submit. Packing happens at submit and is one packer parameterised by the kind, not a fork per kind: bundle (many source files), document (one JSON or LMX doc) or asset (bytes). Only the sandboxed family gets a generated HTML shell.

Trap: [critical] A WRONG itemType NEVER ERRORS — it is clamped to app. Unknown kinds and the three build-only kinds (game, circuit, arduino) all silently become app, on the way in AND on the way out, so a mis-typed listing packs with the wrong extension list, runs the wrong readiness check, renders the wrong preview and installs into the wrong place — consistently, with no error anywhere. The kind is set once in the first step of the wizard and is worth checking on the stored row before submitting.

Know what an install actually does, lane by lane

  1. The lane is read from the STORED listing row, never from anything the submission carries — a publisher cannot describe their own arrival, or they could ask to be written somewhere they were never allowed.
  2. SANDBOX (app, os-app): nothing further is delivered. The install row plus the bundle IS the app; it mounts from its own bytes inside an opaque-origin frame, and arrives switched ON because the browser is the confinement.
  3. MACHINE (mcp-server): also nothing further. The gateway resolves hosted servers from the install rows themselves, so the install is the registration; a second record would be a second source of truth.
  4. ASSET (media, digital-product): nothing further either. The buyer gains the RIGHT; the bytes stay in the signed bundle and are served from the library.
  5. DOCUMENT (workflow, grid): the entry file is parsed as JSON and stored as a document in a domain the buyer already has, usable immediately. An entry that does not parse fails delivery with the message the parser itself gave rather than landing broken.
  6. DYNAMIC (server, router, script, schema, component, input): the entry is stored VERBATIM as source — parsing it would mean evaluating it — into the matching Dynamic collection, and the row arrives enabled: false. The buyer switches it on.

Trap: Delivery NEVER throws: a lane that cannot deliver returns a reason and is logged, so a paid install is never left half-done. Uninstalling removes a delivered row only if the store is what wrote it, so a row the operator authored by hand in the Dynamic layer survives an uninstall of something that happens to share its id.

Add a brand-new KIND to the store

  1. This is a monorepo change, not a listing — you are extending the registry every other answer here is generated from. Read shared/services/knowledge/build-knowledge/build-an-item-type.md first; it is the checklist and this lane deliberately does not duplicate it.
  2. One folder under the item-types package holding one definition, one line added to the registry (declaration order IS the order of the tiles in the wizard), a boilerplate folder that actually contains the declared entry file, and a preview renderer if none of the existing ones fit.
  3. Choose the FAMILY, not the security flags: whether stranger code executes and whether it arrives switched off are properties of the family and cannot be declared per kind.
  4. pnpm check:item-types proves the parts line up — folder and registry agree both ways, the install path resolves to a real Studio tab, the scaffold folder and entry file exist, and the eager half stays React-free so the API can import it.

Trap: A missing boilerplate folder is otherwise SILENT: the scaffolder treats an unknown template as "write the blank starter", so the kind appears to work and ships an empty project. That is the specific failure the guard exists to catch.

Turn the user's own code into a listed app

  1. Decide the tier by answering ONE question: is this code going into the Leumas monorepo and shipping in the Leumas build? Yes is first-party and runs in the API process. No is sandboxed, and that is every third-party app.
  2. Scaffold it. The generated plugin already runs and already packs - start from it rather than from an empty folder.
  3. Build it in App Builder, which compiles on the real path so the preview is not a different program.
  4. Draft the listing, then submit it. SUBMITTING IS PACKING: the bundle is built at that moment, and a pack failure blocks the submission with 400 pack_failed.
  5. Read the warnings in the response. A bundle can pack cleanly and still be broken.
  6. Staff review approves it, writes the price rule and signs the bundle. Only then can anyone install it.

Trap: [critical] You do not write index.html or leumas.app.json. Both are REGENERATED from the stored listing row and yours are dropped with a warning - which is exactly what stops an app granting itself permissions by editing a file it controls. Also: only root-level CSS is linked into the generated shell, so a stylesheet in a subfolder packs silently and renders as nothing.

Ask for the right permissions

  1. Declare only what the app actually calls. The vocabulary is closed and an unknown value throws at definition time, before anything is published.
  2. To call an existing Leumas capability, name it exactly: one entry per system-and-function, not a whole system.
  3. To read a collection the app does not own, name that collection.
  4. To reach the network, name the host.

Trap: An ungranted permission is ABSENT from the app's context, not refused - the failure surfaces as a missing property, and over the proxy as a 404, deliberately, so an app cannot probe what an operator holds. Adding a permission in a new version does NOT grant it: the operator is asked to re-consent and the new version runs on the old grant until they do.

Charge for an app

  1. Declare the intent on the app: free, a subscription naming an EXISTING plan, or metered with the targets that are actually expensive.
  2. Understand that this is only the store card. Nothing has been charged yet.
  3. The enforced price is a separate rule row, written by the review desk when the app is approved - and by boot hydration for first-party apps.
  4. Check what a call will cost with the quote endpoint before telling a user a number.

Trap: [critical] Declared price and enforced price are different things written by different people. The rule's payee comes from the STORED listing, never from the submission - otherwise an app could name somebody else as the person who gets paid. Meter the expensive ACT, not the page: metering something that runs continuously produces a bill nobody can predict.

Get paid

  1. Earnings are read off the ledger, per app, with the platform fee already accounted for.
  2. Withdrawable is not the same as earned: recent earnings are held, prior payouts are subtracted, and the result is clamped to the tokens actually in the wallet.
  3. Connect a payout account, then request a withdrawal.

Trap: A publisher who SPENT their earnings inside Leumas has earnings and no balance - the clamp is what stops the ledger funding the same tokens twice. The fee rounds DOWN, always in the author's favour, and first-party listings carry no fee at all by construction.

Tabs

Every one is a real URL. A tab with its own section below carries a real job.

TabPathGroupWhat it is for
marketplace/admin/marketplace-Browse and install. The index of this domain and its front door.
plugins/admin/plugins-What is running here, and what each one was granted.
purchased/admin/store/purchased-Everything this user has acquired, and where each thing actually landed.
app-builder/admin/app-builder-Build the app itself - chat, code and a live preview on the real compile path.
my-listings/admin/store/mine-The publisher lane: draft, submit, and follow a listing through review.
payouts/admin/store/payouts-Earnings, holds and withdrawals.

Tabs in focus

marketplace - Marketplace

Browse and install. The index of this domain and its front door.

Address: /admin/marketplace

Read with: GET /marketplace/listings | GET /marketplace/listings/:appId

Write with: POST /marketplace/install/:appId

  1. Search or filter the catalog - never work from a remembered list.
  2. Read the listing for its permissions before installing anything.
  3. Install records exactly the permissions the operator granted.

Trap: Installing a first-party app can switch on a whole Studio domain. Core domains have no listing at all and cannot be installed or uninstalled - a request to uninstall one answers 409.

plugins - Installed

What is running here, and what each one was granted.

Address: /admin/plugins

Read with: GET /marketplace/installed | GET /api/plugins/manifests

Write with: POST /marketplace/installed/:appId/enabled | POST /marketplace/uninstall/:appId

Trap: A plugin with no published build is filtered out of the manifest list entirely - which is the usual reason an installed app never appears in the sidebar. Disabling is per user and reversible; uninstalling drops the app's own namespaced tables.

purchased - Purchased

Everything this user has acquired, and where each thing actually landed.

Address: /admin/store/purchased

Read with: GET /marketplace/purchased

Trap: It exists because the store sells many KINDS of thing that arrive in different places - a bought item is not always an app in a sidebar.

app-builder - App Builder

Build the app itself - chat, code and a live preview on the real compile path.

Address: /admin/app-builder

Trap: The preview compiles the way the bundle will, so what renders here is what packs. That is the point of building here rather than in a scratch folder.

my-listings - My Listings

The publisher lane: draft, submit, and follow a listing through review.

Address: /admin/store/mine

Read with: GET /marketplace/mine

Write with: PUT /marketplace/mine/:appId | POST /marketplace/mine/:appId/submit | POST /marketplace/mine/:appId/screenshots

  1. Draft the listing. Source and status are forced - a submission cannot claim to be first-party.
  2. Submit. This packs the bundle, and the response carries both the build and its warnings.
  3. Wait for review. Approval is what writes the price and signs the bundle.

Trap: Submitting is packing, so every pack limit applies at that moment: a file count ceiling, a per-file size ceiling and a total size ceiling, each answering 413. Symlinks are skipped rather than followed. Files with an extension outside the allow-list are dropped with a warning, not an error.

payouts - Payouts

Earnings, holds and withdrawals.

Address: /admin/store/payouts

Read with: GET /marketplace/mine/earnings | GET /marketplace/mine/payouts

Write with: POST /marketplace/mine/payouts | POST /marketplace/mine/payouts/account

Trap: Two fee numbers appear here and they answer different questions - what this publisher gave up, and what the platform collected. They are not comparable and must never be presented as one.

Tools an agent can call here

These are live page capabilities: they exist only while that surface is open, and they call the page's own mutators, so a change made through one is a change the operator can see. A tool missing from this list is not callable — reach for the API or an adapter instead.

unnamed scope

Source: plugin-studio/AppBuilder.jsx

1 more tool(s) on this scope are named at runtime (a computed key, or built by a helper), so they cannot be listed here. Call listTools on the live page to see them.

Which kind of thing to make

Every kind belongs to one of five arrival lanes, and the LANE is what decides how a purchase reaches the buyer and whether it arrives switched on. Pick the lane first and the kind falls out of it. The rows below are generated from the registry, so this table is the count.

KindFamilySell?What it isYou writeIt lands inReach for it when
appsandboxyesA screen inside Studio, plus the actions behind it. Runs sandboxed in a frame.surface.jsxStudio › Installed /admin/pluginsThe default, and the right answer whenever the thing has a SCREEN somebody opens inside Studio. It is the only sellable kind that can carry both a UI and the actions behind it.
os-appsandboxyesA window on the Leumas OS desktop — opens from the launcher, alt-tabs like everything else.app.jsxLeumas OS › Desktop /dashboardThe same code, but the operator wants a WINDOW they alt-tab to rather than a page inside Studio. Choose it for something used alongside other work, not inside a workflow.
serverdynamicyesA whole server or proxy target that runs beside your instance. Arrives switched off.server.jsDynamic › Routers /admin/dynamic/routers (off on arrival)You already run the thing and want Leumas to put it behind an address. Reach for it when the code cannot or should not move.
routerdynamicyesA set of HTTP endpoints mounted into an instance. Arrives switched off.router.jsDynamic › Routers /admin/dynamic/routers (off on arrival)A handful of HTTP endpoints, and no UI at all. Choose this over an app whenever nobody needs to look at it — a webhook target, an integration shim, a machine-facing API.
scriptdynamicyesAn LMX script that runs sandboxed on an instance. Arrives switched off.main.lmxDynamic › Scripts /admin/dynamic/scripts (off on arrival)One job, written once, run on demand or on a schedule. Choose it over a workflow when there are no branches and nothing to wait for.
schemadynamicyesA typed collection — fields, validation and a generated form, ready to fill.schema.jsonDynamic › Schemas /admin/dynamic/schemas (off on arrival)The value is the SHAPE — fields, validation and a form somebody fills in. Ship this instead of an app whenever the buyer needs to hold records, not run code.
componentdynamicyesA React component you can drop onto any page. Arrives switched off.Component.jsxDynamic › SSR /admin/dynamic (off on arrival)One React component that drops onto a page the buyer already has. Choose it over an app when it is a PART of a page rather than a page of its own.
inputdynamicyesA control that collects one value, usable in every form on an instance.Input.jsxDynamic › Inputs /admin/dynamic/inputs (off on arrival)It collects exactly ONE value and should then be available in every form on the instance. If it renders a whole panel it is a component, not an input.
workflowdocumentyesSteps wired together — runs on a trigger, a schedule or a button.workflow.jsonDynamic › Workflows /admin/dynamic/workflowsSeveral steps with branches, waits or approvals between them. Choose it over a script the moment anything has to happen conditionally or later.
griddocumentyesA 3D programming board — layers of cells that compute from each other.grid.jsonProgramming › Grids /admin/programming/gridsThe work is a computation over a board of cells that feed each other. Reach for it when the answer is a model rather than a procedure.
gamedocumentbuild onlyA multiplayer 3D game — rules, a level and a manifest, on the Leumas game engine.game.jsAI › Games /admin/d/aiBuilt and played on the Leumas game engine. Not sellable in the store today — build it, do not plan a listing around it.
mcp-servermachineyesTools any AI agent can call — Claude, Cursor, your own Leviathan.server.jsonMCP Servers /admin/d/ai/mcpYou want any AI agent — not just Leviathan — to be able to call your tools. This is the only kind whose audience is a machine.
mediaassetyesModels, audio, video, images or fonts. Buyers download them and own them.pack.jsonFiles › Library /admin/filesFiles the buyer downloads and owns: models, audio, video, images, fonts. Nothing runs.
digital-productassetyesTemplates, presets, datasets, documents — anything sold as a download.product.jsonFiles › Library /admin/filesThe same lane as media, for things that are not media — templates, presets, datasets, documents. Pick by what the buyer thinks they bought.
circuitdocumentbuild onlyA PCB designed in Circuit Studio.circuit.tsxCircuit Studio /admin/circuitA board designed in Circuit Studio. A project kind, not a listing — the fabrication lane quotes it, it is not sold here.
arduinodocumentbuild onlyA sketch built in the Leumas Arduino IDE.sketch.inoArduino IDE /admin/arduinoA sketch built in the Leumas Arduino IDE. Like circuit: buildable, not sellable.

Traps

The marketplace router is mounted at /marketplace, NOT /api/marketplace — every store call in this lane is written the way it must actually be sent. Where a purchase LANDS is derived from the registry on every read, never stored on the row. That is why the destination in the table above is always current and why a listing cannot nominate its own. The whole-app lane — scaffolding, the sandbox traps, permissions, pricing shapes and the publish flow — is documented in the build-leumas-plugin skill; hosting an already-built site is a DIFFERENT product with a different lifecycle and lives in host-a-vibe-coded-app. Neither is repeated here. Third-party code never runs in the API process. That is not caution, it is the reason the permission system means anything. The sandbox frame is deliberately origin-less: an app cannot read cookies, cannot use browser storage directly, and cannot call the API except through the one proxy, whose dispatch table is its entire surface. Never widen that sandbox. Nothing sensitive crosses the frame bridge, because the frame cannot be addressed precisely enough for that to be safe. An unsigned build is served to nobody - the signature is the artefact of review, not of packing. Three axes, never conflated: PERMISSIONS are what an app's code may touch (absent when ungranted); ENTITLEMENTS are what the plan allows (402 when short); MEMBERSHIP is whether the user subscribes at all. Install state is a fourth, and answers 404.

Source .claude/skills/leumas-studio/reference/domains/apps.md (no-git)markdownjson
Generated from the Leumas repository. Every page cites the file it came from.leumas.techllms.txt