Create a plugin

A plugin is a folder of mesh nodes in a git repo. There is no project file, no NuGet package and no app rebuild: the mesh imports the folder and compiles the C# live with Roslyn. The node is the manifest — there is no bespoke package.json.

If you take one thing from this page: the plugin root lives at <Plugin>/index.json, INSIDE the folder. A sibling <Plugin>.json outside it is not part of the partition, so the plugin imports without its root and fails to install. This is the single most common first-attempt failure.

The layout

MyPlugin/
  index.json                  ← the plugin root: nodeType "Store/Plugin", content { $type: PluginContent }
  Guide.md                    ← a Markdown doc node (this page is one)
  Widget.json                 ← a NODE TYPE: nodeType "NodeType", content { $type: NodeTypeDefinition }
  Widget/
    Source/                   ← the C# the mesh COMPILES LIVE on import
      WidgetContent.cs        ← the content record
      WidgetLayoutAreas.cs    ← the views
    Test/
      WidgetTests.cs          ← compiled into the SAME assembly as Source/, so it calls the type directly
  manifest.lock               ← machine-maintained; regenerate, never hand-edit

Every node is one file. X.json is a node; its children live in the sibling folder X/. (A GitSync export writes a node that has children as X/index.json — import accepts both forms.)

The three kinds of node

The root<Plugin>/index.json, nodeType: "Store/Plugin". Its PluginContent carries description (the long blurb), optional body (the authored cover), poster/video for the cover page, entryPoint, documentation, publicSegments and minMeshVersion. The node itself carries name, description (the one-line card tagline), an inline-SVG icon, and category — the key the /Store browse page groups by.

The icon speaks ONE language across every card — a colored pictorial mark: viewBox='0 0 24 24', a full-bleed brand-hue backplate (<rect width='24' height='24' rx='5' fill='#0369a1'/>) with white detail and strokes around 1.2–2, the motif kept literal (Mail is an envelope, AppleMaps is the pin). Never a currentColor-only outline: it has no color of its own, so it inherits the card's text color and vanishes on one theme — store-presentation.py reports exactly that as a polish note on every PR. Worked examples: Mail, AppleMaps, Import, Observability. The same icon IS the page's favicon — the portal publishes the current node's icon to the browser tab, so keep the mark legible at 16 px: one bold plate, one simple detail.

The backplate is structural, not just style (core #2083): an inline-svg icon that does NOT paint its own full-bleed plate gets one GENERATED at the render seam — hue hashed from the markup, currentColor recolored white — so no icon can render invisibly in dark mode. Author the plate anyway: the author picks the hue, the generator only picks a hue, and an authored mark passes through byte-identical.

Translations are separate spaces. A plugin/course in another language is its OWN Store/Plugin space (own partition, price and entitlements) — never mixed-language content in one space. The roots link each other via language (the BCP-47 tag of the content, e.g. en, de-CH) and translations (tag → the twin's root path, kept symmetric on every twin); the cover then renders a compact 🌐 switcher linking the other languages' covers.

price only if it is genuinely for sale. price is a number and null means not purchasable and never gated. Setting it turns on the whole funnel: anonymous cover page, auto-gated children, a {plugin}/Subscribe paywall and entitlement records. Leave it out unless you mean it.

A NodeType<Plugin>/<Type>.json, nodeType: "NodeType". Its NodeTypeDefinition carries a configuration C# lambda:

config => config.WithContentType<WidgetContent>().AddDefaultLayoutAreas().AddLayout(l => l.AddWidgetLayoutAreas())

Set includeGlobalTypes: true so the compile sees the framework's types.

The code<Type>/Source/*.cs provides everything the lambda references, and <Type>/Test/*.cs the tests. On import the mesh compiles both together; the type goes green (CompilationStatus.Ok) and is usable immediately. Recompiles happen on a release request, never automatically.

Rules the live compile enforces

Your Source/ is compiled by the mesh, against the framework assemblies, with nullable and warnings-as-errors. That imposes constraints a normal project does not:

The gates

Every one of these is a hard PR gate. Run them before you push.

Step Command What it proves
1. Shape python3 scripts/validate-repos.py Every node JSON is a well-formed MeshNode with id + nodeType, and every NodeType has a Source/*.cs. Shape only — it compiles nothing.
2. Compile python3 scripts/compile-check.py Compiles every NodeType exactly as the mesh does. Needs framework assemblies — it auto-discovers a sibling ../MeshWeaver checkout's built src/*/bin, or pass --refs <dir>.
3. Manifests python3 scripts/gen-manifests.py Refreshes manifest.lock after any change to a plugin folder. CI fails on a stale one; --check says which drifted.

Passing validate is necessary, never sufficient. A NodeType whose Source/ does not compile is not merely broken — it is a time bomb. The mesh serves the last-good cached assembly until a pod restart wipes the cache, then recompile-on-activation fails and the type parks: every hub of it faults and its pages throw. That is exactly how async-broken source sat green in this repo for days and then took down a live type on two meshes after a restart. The compile gate exists because nothing else catches it.

Test every feature, and run the tests. Tests live in <Type>/Test/*.cs as plain public static methods that throw on failure. Cover each invariant, branch and boundary — one happy-path case is not "tested". Also expose a Tests layout area that runs every case and renders a pass/fail table, so the suite can be executed on a mesh and asserted in CI. Compiling the test files is not the same as running them.

Try it on a mesh

Because the mesh compiles Source/, you do not need the core solution to author a plugin. Import the repo into a local mesh (the memexlocal MCP), then get the type to confirm it reaches CompilationStatus.Ok, render_area a view, and render its Tests area to confirm every case is green. Do not verify against a shared or production instance.

Publish and install

Merge to main and the plugin becomes installable. The registry instance imports it once with its own GitHub App credential and re-serves it, so every other mesh installs without needing its own GitHub access — the same credential encapsulation npm and NuGet provide. Installing is just importing the node repo, which parses each *.json into a node and compiles the NodeTypes.

Provisioning runs under the SYSTEM identity

The store catalog's Provision action (the admin button on a package-source card) and the headless entry point behind it — SystemInstall.Run(hub, request) in Store/Publishing/Source/ SystemInstall.cs, callable from any hub compiled with that source — install a package through one engine with three guarantees:

The phased chain (see ProvisionPlan): Authorize (real invokers only) → check declared upstream modules → create root (absent roots only) → GitHub sync → Import → check derived upstream modules → Compile types. The catalog binds the run to a live progress bar on the card (stage label + completed-phase fraction, distinct error state, completion pointer) via the ProvisionRunState data stream — feedback is immediate on click and reactive throughout.

For the full picture read Doc/Architecture/Plugins (the mechanism), Doc/Architecture/PluginAuthoring (author → publish → install, end to end), Doc/Architecture/PluginRegistry (how the registry serves them) and Doc/Architecture/NodeTypeCompilation (what the live compile does).

Start from a working example

Copy the closest one rather than starting blank: Slides (a single type), Edu (four types, including cross-type reads) and LinkedIn (thirteen types plus CSV loaders).

Reconnecting…
The connection to the server was interrupted. Trying to restore it…
Trying again…
The connection could not be restored. Reloading the page…
The server was updated. Reloading the page to pick up the latest version.