Skip to main content

πŸ“ The CMS example

A content management system where every feature is a plugin, delivered by two Python packages β€” a free one and a paid one β€” and drawn with shadcn/ui.

Run it on this site β†’ β€” including the part that matters: installing the paid package and watching three plugins appear.

CMS
β”‚
β”œβ”€β”€ Python package: cms (free β€” and the application itself)
β”‚ └── Extension: Core
β”‚ β”œβ”€β”€ Markdown Tools β†’ Editor Toolbar
β”‚ β”œβ”€β”€ Gallery β†’ Content Types
β”‚ └── SEO Validator β†’ Publish Lifecycle
β”‚
└── Python package: cms-pro (paid)
└── Extension: Pro
β”œβ”€β”€ AI Writing Assistant β†’ Editor Toolbar
β”œβ”€β”€ Product β†’ Content Types
└── Social Publisher β†’ Publish Lifecycle
ConceptHere
ApplicationCMS
Python packagecms, cms-pro
ExtensionCore, Pro
PluginGallery, SEO Validator, AI Writing Assistant…
Contribution pointEditor Toolbar, Content Types, Publish Lifecycle
Contributiona toolbar button, a content type, a publish step

The hierarchy, which is the thing to hold on to:

Python package β†’ Extension β†’ Plugin β†’ Contribution β†’ Contribution point

Run it​

make cms # build the interface, install the free tier
datalayer-cms # http://localhost:8788

Then, while it is running:

make cms-pro # or: pip install examples/cms/cms-pro
# refresh the browser

Three more plugins appear in the same three points β€” a Rewrite button beside Heading/Bold/Link, a Product type beside Gallery, a Social step beside SEO. No restart, no rebuild of the interface, and no change to cms.

note

A regular pip install, not pip install -e. An editable install writes a .pth file that Python only processes at interpreter startup, so an editable package genuinely does need a restart. A normal install lands in site-packages, which a running process can be made to re-read β€” see packaging.

Packaging is independent of extensibility​

This is the reason the example has two packages.

There is no plugin API for paid plugins. No capability flag, no tier check, nothing in the host that knows one package was bought. cms-pro advertises itself under the same entry-point group as cms, registers on the same platform, and contributes to the same three point ids. Read cms-pro/cms_pro/plugins.py beside cms/cms/plugins.py: they are the same kind of file.

What makes it "pro" is who may download the wheel β€” a question about distribution, answered entirely outside Reactor. That separation is what lets a third party write a plugin without asking anybody's permission, and it is what a marketplace needs to be possible at all.

cms is deliberately both the application and an extension: Core is discovered through the same group as Pro, so the host has no privileged idea of its own plugins.

Three points, three shapes​

One mechanism, three different interactions β€” so that "contribution point" does not get read as "list of buttons":

PointThe application…
cms.editorToolbarrenders every contribution, as a button
cms.contentTyperenders a chooser and shows one
cms.publishLifecycleruns every contribution, and any one can veto

The SEO validator refuses a publish that would be invisible; the social publisher only announces one and never blocks. Both fill the same point β€” which is why a lifecycle whose steps could only veto, or only observe, would have been the wrong design.

The plugins do not know what the CMS looks like​

Almost every contribution here is a plain record: a label and a function. The application draws them, so a plugin never touches a design system.

The exception is the AI assistant, which contributes a panel β€” and draws it with shadcn/ui components it never installed. The host publishes its design system alongside React:

setReactorSharedModules({
react: React,
'@datalayer/reactor': Reactor,
'@cms/ui': UI, // ← the host's kit, handed to its plugins
});
// in the plugin
const ui = shared['@cms/ui'] ?? {};
const Card = ui.Card ?? 'div'; // a host that publishes none still works

That is the answer to "is the plugin model independent of the UI kit?" β€” not "plugins avoid the kit", because some of them must draw, but the kit is something the host hands over. The music store is the same model with Primer, and its plugins are the same shape.

A trap worth knowing

A plugin cannot ship Tailwind classes of its own. Tailwind generates CSS by scanning source at build time, so a class appearing only inside a module fetched at runtime is a class nobody generated. Publishing the components sidesteps it: every class lives in the host's build.

What it demonstrates, in one list​

  • One pip install, both tiers β€” each package ships its Python plugins and their browser halves.
  • A host β€” one command serves the interface and the API from one origin.
  • Remote plugins β€” every plugin arrives from a URL the server named; the application bundles none.
  • Extensions group without governing β€” Core and Pro each deliver three plugins, and every plugin is still switched on its own.
  • Commands on both tiers β€” Ctrl-K opens the palette over the application's own document commands, while the Python packages register theirs for the command line.

Commands, and what installing changes​

Core's Python package ships a cms command group and Pro ships a pro one, so the command line grows and shrinks with what is installed β€” the same claim the browser half makes, made where there is no browser:

reactor cms check "A title that is long enough" --description "…"
pip install cms-pro
reactor pro rewrite "hello" # only after Pro is installed