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 acurrentColor-only outline: it has no color of its own, so it inherits the card's text color and vanishes on one theme —store-presentation.pyreports exactly that as a polish note on every PR. Worked examples: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,
currentColorrecolored 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/Pluginspace (own partition, price and entitlements) — never mixed-language content in one space. The roots link each other vialanguage(the BCP-47 tag of the content, e.g.en,de-CH) andtranslations(tag → the twin's root path, kept symmetric on every twin); the cover then renders a compact 🌐 switcher linking the other languages' covers.
priceonly if it is genuinely for sale.priceis a number andnullmeans not purchasable and never gated. Setting it turns on the whole funnel: anonymous cover page, auto-gated children, a{plugin}/Subscribepaywall 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:
- Reactive, never async.
IObservable<T>end to end. Noasync/await/Task<T>/.Resultin hub- or view-reachable code; compose with.Select/.SelectManyand.Subscribe, and route real I/O throughIIoPool. SeeDoc/Architecture/AsynchronousCalls. - UI is framework controls, never hand-built HTML. Build with
Controls.*bound to node streams. Never emit HTML strings, and never replicate a node into a/datacopy plus a save loop. SeeDoc/GUI/DataBinding. - Explicit
usings in every file — there are no implicit usings in a live compile. - Only public framework surface. Internal helpers are invisible to your assembly; inline an equivalent.
- Self-contained
Source/. Reference only core framework types (plusincludeGlobalTypes), so it compiles on any mesh. - Immutable collections (
ImmutableList,ImmutableDictionary) and nostaticmutable state. - A type's identity is its install path (
MyPlugin/Widget). Derive "my type" from the node — never hardcode a name, because the plugin can be installed anywhere. - Read foreign content untyped. A sibling type from another assembly arrives as
JsonElement; read its fields off the JSON.
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:
- System-owned, no personal grants. Every node write runs under
AccessService.ImpersonateAsSystem(). The invoking admin is recorded on the import activity as attribution only — they never become the partition's creator/owner, so provisioning never hands them a root_Accessgrant thatEntitlementswould read as a purchase. Access to the installed content flows exclusively from the Store/Plugin gating (public cover,publicSegments, entitlements, coupons, orders). A real (non-system) invoker is verified to be a global admin in the plan's first phase before anything is written. - The root is
Store/Plugin-typed from the start. The engine provisions the partition's backing store through the canonicalIPartitionStorageProvider.EnsurePartitionProvisionedand creates the root asnodeType: "Store/Plugin"carrying aPluginContentseeded from the package's own manifest (index.json) — never the historicalSpaceplaceholder that the import re-typed later. That detour left roots indexed asSpace, sonodeType:Store/Pluginqueries silently missed the plugin, and it granted the clicking admin creator Admin (defect 1 above). - Idempotent — a re-run RECONCILES. Running install on an existing partition never re-creates
or clobbers the root; it merges the
{plugin}/_GitSyncwiring in place (repositoryUrl / branch / subdirectory updated, sync bookkeeping likelastSyncedAtpreserved), re-runs the import and the compile gate. A create that races a concurrent install degrades to the same reconcile.
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).