π 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
| Concept | Here |
|---|---|
| Application | CMS |
| Python package | cms, cms-pro |
| Extension | Core, Pro |
| Plugin | Gallery, SEO Validator, AI Writing Assistant⦠|
| Contribution point | Editor Toolbar, Content Types, Publish Lifecycle |
| Contribution | a 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.
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":
| Point | The application⦠|
|---|---|
cms.editorToolbar | renders every contribution, as a button |
cms.contentType | renders a chooser and shows one |
cms.publishLifecycle | runs 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 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