The feature that lives in three places

Most "add a feature" tickets sound simple until you notice where the feature actually has to appear. A booking system is not one thing. It is an API that holds availability and confirms slots, an admin screen where staff manage the calendar and pricing, and a customer-facing widget where someone picks a time and pays. Three surfaces, one idea. Miss any one of them and the feature is half-built.

On VBWD this pattern has a name: the plugin bundle. Instead of pretending a feature is a monolith, a bundle is honest about the fact that a real feature spans the backend, the admin app, and the user app. It packages those three coordinated parts so they ship, enable, and version together. This article walks through what a bundle is, the three pieces that compose it, a worked example, the seams it plugs into, and the costs you take on when you use one.

What a bundle actually is

VBWD is a self-hosted, source-available platform. The backend is Python and Flask on PostgreSQL, the frontends are Vue 3 with TypeScript, and everything runs under Docker. The design goal underneath all of it is an agnostic core: the platform ships routing, auth, billing primitives, a plugin loader, and a set of extension points, and it knows nothing about shops, bookings, or tarot readings. Domain knowledge lives entirely in plugins.

A bundle is how one feature gets delivered across the whole stack without touching that core. Concretely, a bundle is three coordinated parts:

Each part is its own repository with its own build and its own deploy, but they are designed as a set. The plugin catalogue lists bundles like shop, booking, subscription, ghrm, and cms exactly this way — one feature, coordinated across the apps that need to render it.

The three coordinated pieces

Backend: the part that owns the truth

The backend plugin is where the feature is real. It defines the SQLAlchemy models, the repositories that read and write them, the services that hold the rules, and the routes that expose them under /api/v1/. It ships its own Alembic migrations so the schema travels with the code rather than being applied by hand.

Backend plugins are auto-discovered. Drop the plugin into the backend's plugin directory, mark it enabled, and the loader finds it, registers its blueprint, and wires its dependencies. You do not edit a central registry file in core to add a feature. That discovery model is what keeps the core agnostic: the platform never names your plugin, it just scans for plugins and lets them announce themselves. The architecture overview describes this loader and the layered Routes → Services → Repositories → Models flow that every backend plugin follows.

fe-admin: the operator's control surface

The admin plugin is the screen your operations team lives in. For a shop that is product and inventory management; for booking it is the calendar, resources, and pricing rules; for subscription it is plans and their lifecycle. It talks to the backend plugin's admin routes and nothing else — it is a client of the same API, not a second copy of the logic.

Keeping admin UI in its own plugin matters for a practical reason: the people configuring a feature and the people using it have completely different needs, permissions, and failure modes. Splitting them means you can lock the admin surface behind role checks without dragging that weight into the customer path.

fe-user: the customer-facing surface

The user plugin is what your customers see: the storefront, the booking widget, the account and subscription screens. It registers its own routes, its own Pinia stores, and its own translations into the user app, and it renders against the public slice of the backend plugin's API.

This is also the surface most tied to your brand. Because it is a plugin rather than a core edit, you can restyle it, translate it, or swap it without forking the platform, and you can enable it per-instance depending on which features a given deployment sells.

A worked example: the shop bundle

Say an agency is standing up an online shop for a client. Reaching for the shop bundle, here is what each part contributes.

Backend. The shop plugin brings Product and order models, a product-type system, repositories, and services that price a cart and turn it into an invoice. It exposes admin routes for catalogue management and public routes for browsing and checkout. Its migrations create the shop tables on first enable.

# backend plugin, registering itself against core seams
class ShopPlugin(BasePlugin):
    @property
    def metadata(self) -> PluginMetadata:
        return PluginMetadata(name="shop", version="1.0.0")

    def on_enable(self):
        # add repositories/services to the DI container
        self.container.product_repository.override(...)
        # subscribe to core events, e.g. payment.captured
        self.event_bus.subscribe("payment.captured", self.fulfil_order)

fe-admin. The shop admin plugin adds a Products section to the backoffice: create and edit products, manage stock, set prices. Every action is an API call to the backend plugin's admin routes.

fe-user. The shop user plugin adds the storefront and cart routes, a cart store, and checkout translations to the user app:

export const shopPlugin: IPlugin = {
  name: 'shop',
  version: '1.0.0',
  install(sdk) {
    sdk.addRoute({ path: '/shop', name: 'Shop',
      component: () => import('./ShopView.vue') })
    sdk.createStore('cart', { state: () => ({ items: [] }) })
    sdk.addTranslations('en', { shop: { checkout: 'Checkout' } })
  },
}

Three repositories, one shopping experience. The customer never knows there is a seam between the storefront and the admin catalogue; the operator never touches the storefront code to change a price. Checkout and payment ride the platform's own billing primitives, so the shop bundle spends its effort on products and carts rather than reinventing invoicing.

The seams a bundle plugs into

The reason a bundle can do all this without editing core is a small set of documented extension points. A backend plugin reaches for whichever it needs:

On the frontend, the parallel seam is the plugin registry and SDK. Both the admin and user apps expose an SDK through which a plugin adds routes, creates stores, and registers translations. The app calls installAll and then activates each plugin; the plugin never edits the app's router or store setup directly. You can read the shape of these APIs in the developer docs, and the broader menu of what ships as bundles versus core lives on the features page.

One detail ties the three parts together at runtime: enable-state. Whether a plugin is on or off is stored in a single directory on the host, shared by all three apps via bind mounts. The backend is the only writer; the frontends read it as read-only. That means "the shop is enabled" is one fact, not three that can drift out of sync between browsers or containers.

Honest limits

A bundle is the right shape for a real feature, but it is not free. Three things are worth knowing before you commit to one.

The three parts must stay version-coordinated. If the backend plugin changes an admin route's contract, the fe-admin plugin has to move with it. Nothing stops you deploying a new backend and a stale frontend; the platform will not catch that mismatch for you. You need a release discipline that ships the set together.

A bundle is more setup than a single-file plugin. Not every extension needs three parts. A backend-only plugin — a search provider, a webhook relay, an MCP endpoint — can be a single small plugin with no UI at all. Reach for a full bundle only when the feature genuinely has a customer surface and an operator surface. Using a bundle for something that could have been one file is overhead you will pay at every deploy.

You own deployment of all three. VBWD is self-hosted. That is the point — your data, your infrastructure, no vendor in the request path — but it also means the three repositories, their builds, and their enable-state are yours to orchestrate. The platform gives you the seams and the discovery; it does not run your servers.

Takeaway

Think of a feature the way your users and operators experience it, not the way a single service happens to store it. A booking is a slot in the database, a calendar for staff, and a widget for customers, all at once. A plugin bundle names that reality and packages it: one backend plugin that owns the truth, one admin plugin to configure it, one user plugin to present it, held together by auto-discovery, a handful of typed seams, and a single shared enable-state. The cost is coordination across three repositories. The payoff is that you extend the platform without ever forking it, and the core stays as ignorant of your domain as the day you installed it.

On the licensing side, none of this puts you behind a paywall to start. VBWD is source-available under BSL 1.1 and free to run commercially while your annual VBWD-attributable sales stay under the value of 6.7 BTC, and the licence converts to Apache-2.0 over time. You can build and ship bundles on that footing without asking anyone.

Planning a feature that spans all three apps? Start by mapping it onto the three bundle parts, then check the plugin catalogue to see whether an existing bundle already covers most of it.