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 server was updated. Reloading the page to pick up the latest version.