Developer Docs · 04

Core-Agnosticism & Extension Seams

The core never imports a plugin. Plugins extend it only through the seams the core deliberately exposes: ports, registries and DI.

The rule

Core resolves standalone. No core module imports from plugins.*; no core migration is anchored on a plugin; the core knows nothing about “subscription”, “shop” or any other vertical vocabulary.

This is enforced by an AST oracle (tests/unit/test_core_agnosticism.py) that walks vbwd/ and fails the build on any plugin import, plus a vocabulary oracle that bans domain words leaking into core.

A port may live in core only if core routes through it

The litmus test: a port (interface) may live in core only if the core ROUTES through it. If core would have to READ a domain field, that's a leak and the concept belongs in a plugin. Money flows out of core as bus events and line-item handlers, never as direct calls into a vertical.

The seams plugins use

  • Ports / interfaces — e.g. ISubscriptionLifecycle, IWithdrawableBalance; a plugin implements them, core depends only on the abstraction.
  • Registries — the line-item handler registry, the checkout-source registry, the data-exchange registry, the entity-type registry, the deletion-dependency registry, fe-admin extension registries.
  • DI providers — a plugin's on_enable adds its repositories to the container (providers.Factory(Repo, session=container.db_session)); core declares none.

Example: extending the payment line items

# In the plugin's on_enable:
from vbwd.services.line_items import register_line_item_handler
register_line_item_handler("subscription", my_handler)

# core's PaymentCapturedHandler dispatches to the registered
# handler by item type — it never imports the plugin.

The same shape powers data exchange, checkout sources and the fe-admin injection seams: core iterates a registry; plugins fill it.