Most "extensible" platforms make you pay a tax to extend them. You edit a central registry, touch the application entry point, wire a route table, and pray the next upstream update does not collide with your change. VBWD takes a harder line: you add a feature by dropping a directory into a folder. The core never learns your feature exists, and you never edit the core to teach it. This post explains how that works, why it is enforced rather than merely encouraged, and what it means for your maintenance bill.
VBWD's design has one load-bearing rule. The core — accounts, tokens, invoices, the CMS engine, the plugin machinery itself — knows nothing about any product domain. It does not know what a "subscription," a "dataset," a "booking," or a "tarot reading" is. All of that vocabulary lives in plugins. The core provides mechanism; plugins provide policy.
This is not a style preference you can ignore. It is checked by an automated guard — an oracle test that walks the core's source and fails the build if core code imports from plugins.* or contains domain-specific vocabulary. If someone tries to sneak a plugin concept into core, CI goes red. The discipline is mechanical, so it does not erode over the months the way conventions usually do.
The payoff is direct: because the core cannot depend on any plugin, it always resolves standalone, and any plugin can be added or removed without the core noticing. That is the property that makes "drop a folder" safe.
A backend plugin is a directory under the plugins folder with a predictable shape: an __init__.py, a source package, a tests folder, and a small metadata declaration. At its heart is a class that extends the platform's BasePlugin and describes itself:
from src.plugins.base import BasePlugin, PluginMetadata
class MyPlugin(BasePlugin):
@property
def metadata(self) -> PluginMetadata:
return PluginMetadata(
name="my-plugin",
version="1.0.0",
dependencies=[], # other plugins this one needs
)
def on_enable(self):
# register routes, services, event handlers here
...
def on_disable(self):
# tear down cleanly
...The platform discovers this automatically. There is a lifecycle behind it — a plugin moves through discovered, registered, initialised, and enabled/disabled states — and the manager is dependency-aware, so if your plugin declares that it needs another plugin, the platform initialises them in the right order. You declare dependencies in metadata; you do not hand-sort an import graph.
Crucially, adding this plugin does not require touching the Flask app factory, a master route list, or a central "register all features" function. The plugin registers its own routes and services from inside its lifecycle hooks. The application entry point is something you read, not something you edit.
"Don't touch core" would be useless advice if the core did not give you places to plug in. It does — they are called seams, and they are the intended extension points. A few examples of how real plugins hook in without modifying core:
on_enable adds its own repositories and services to the DI container, so the rest of the app can resolve them by interface. The container is the seam; core owns it, plugins populate it.The pattern is always the same: core defines the shape of the extension point and iterates over whatever is registered; the plugin supplies a concrete implementation. Neither side imports the other's domain concepts.
Consider a plugin that turns a private GitHub repository into a paid product. When a customer's subscription becomes active, they should get access; when it lapses, access should be revoked after a grace period. Here is how that lives entirely outside core:
subscription.activated event on enable.subscription.cancelled/expired and schedules revocation after the grace window.Nothing in the core mentions GitHub, repositories, or deploy tokens. The core only knows that subscriptions have a lifecycle and that it emits events for that lifecycle. Everything domain-specific — the word "repository," the GitHub client, the grace-period policy — is in the plugin. Swap the plugin out and the core is unchanged; the events simply have no subscriber.
One practical rule this example teaches: when a plugin writes to the database inside an event-bus callback, it must commit its own session. The core hands you the event; managing your transaction is your responsibility, because the core does not know a write happened.
Because there are three apps — backend, admin frontend, user frontend — "is this plugin on?" could easily drift between them. VBWD avoids that by keeping enable/disable state in a single directory on the host that all three containers read. The backend is the only writer, via an admin endpoint; the frontends mount the files read-only. There is no per-browser toggle and no localStorage drift. When you enable a plugin, all three apps agree, because they are reading the same file.
A related gotcha worth stating plainly: a healthy server is not the same as a loaded plugin. The API answering with 200 tells you the process is up; it does not tell you your plugin registered. If a feature is missing, check the plugin's enable state and its on-enable registration, not the health endpoint.
The business case for the drop-a-file model is boring and real: it is the difference between a codebase that gets cheaper to change over time and one that gets more expensive. Three specific savings:
Upgrades don't fight your customisations. Because your feature lives in its own directory and never edits core files, pulling a core update rarely produces merge conflicts. You are not maintaining a fork; you are maintaining an addition. That is the single largest hidden cost in most "customised" platforms, and this design removes it.
Features are independently testable and removable. A plugin ships with its own tests and its own migrations (in the plugin, not scattered through a central migrations tree). You can enable it in isolation, test it, and disable it without unpicking it from a monolith. If a client cancels a feature, you turn the plugin off.
The core stays small and auditable. Every platform accretes domain logic into its core over time — unless something actively prevents it. The oracle prevents it. The core remains a general-purpose subscription-and-content engine, which means it is easier to reason about, easier to secure, and easier to keep fast.
This model is not free of constraints, and it is better to know them going in. You extend the core only where it exposes a seam; if the extension point you need does not exist yet, the right move is to add a seam to core (a new registry or event) as a small, general, domain-neutral change — not to reach into core with a domain-specific hack. That is more disciplined than editing a central file, and occasionally slower in the moment. The reward is that the next plugin gets the same seam for free, and the oracle keeps everyone honest. There is also a learning curve: "where is the seam for this?" takes a little time to internalise. But once you have shipped one plugin, the shape is obvious, and every subsequent feature follows the same rhythm.
Extensibility is usually a promise a platform makes and then quietly breaks as its core fills with special cases. VBWD keeps the promise by making it mechanical: the core is forbidden — by an automated test — from knowing what your plugins do, and your plugins reach the core only through published seams. You add a feature by adding a folder, declare what it depends on in metadata, and register everything from inside your own lifecycle hooks. The result is a platform where the second year of development is cheaper than the first, because nothing you built ever forced you to touch the thing everyone else depends on.
To build your first plugin, copy the plugin skeleton, declare its metadata, and register routes and services in on_enable. The core will discover the rest.
