Plugins
Honeyframe's plugin architecture mounts backend routers as of v0.0.34 (tech-debt #7 first cut). The contract, discovery, and runtime mounting work end-to-end. Frontend asset merging and dbt workspace registration are the remaining surfaces.
This page documents what works today, the runtime model, and how to write a plugin that actually plugs in.
Today's state (v0.0.34+)
The plugin host is paas/backend/services/plugin_host.py. It provides:
- A typed
PluginManifestdataclass — the contract. - A
discover_plugins()routine that walks both$INSTALL_DIR/plugins/(bundled, ships with the tarball) and$DATA_DIR/plugins/(customer-installed, persists across upgrades — same split v0.0.32 introduced for.env). - A
mount_plugins(app)helper that imports each manifest'smodule:attrrouter specs andapp.include_router()s them at app-import time. Integration tests that build the app viaASGITransport(without firing lifespan) still see the mounted routes.
A bundled _demo plugin (plugins/_demo/) proves the discover→mount loop end-to-end: it exposes GET /api/_demo/ping returning a 200. Pinned by paas/tests/integration/test_plugin_host.py.
What still doesn't work:
- Frontend bundle merging — plugin frontend pages don't yet load into the Platform's Vite build. Plugin frontends still need a manual mount in dev.
- dbt workspace auto-registration — plugin
dbt_project.ymlfiles are listed in the manifest but not yet scheduled. - install/uninstall/enable/disable —
/api/pluginsis read-only (admin-only, returns name/version/source/router_count/dbt_workspace/errors). Lifecycle is "drop the folder, restart the platform".
A new Plugins admin page (PaaS frontend → Administration menu, between Audit Log and Settings) lists every discovered plugin, its source (bundled for install-dir, customer for data-dir), router count, and any errors. Read-only — see GET /api/plugins.
Where plugins live
| Location | Source label | When to use |
|---|---|---|
$INSTALL_DIR/plugins/<slug>/ | bundled / install | Plugins that ship with the platform (_demo, future first-party verticals). Replaced on upgrade. |
$DATA_DIR/plugins/<slug>/ | customer / data | Customer-installed plugins. Persists across --upgrade. |
Collision rule: if a plugin name exists in both locations, the install-dir version wins. The data-dir version is shadowed and a WARNING is logged. Customers extend, they don't shadow.
Plugin layout
A plugin lives at <INSTALL_DIR>/plugins/<vertical-slug>/ with this layout:
plugins/crm/
├── plugin.yaml ← required manifest
├── backend/
│ ├── routers/
│ │ ├── customers.py ← FastAPI router
│ │ └── accounts.py
│ ├── services/
│ └── middleware/
├── frontend/
│ ├── src/
│ │ ├── pages/ ← additional Platform pages
│ │ └── blocks/ ← additional dashboard blocks
└── dbt/ ← optional dbt workspace
├── dbt_project.yml
└── models/
The plugin name is its directory slug (crm, finance, etc.) — lowercase, hyphenated, must match plugin.yaml's name field.
plugin.yaml
The manifest contract:
name: crm # vertical slug, required
version: 0.1.0 # plugin semver, versions independently of PaaS
display_name: CRM # human label for the admin UI
# Dotted Python paths to FastAPI routers. PaaS will mount each at startup
# once router auto-attach lands.
routers:
- plugins.crm.backend.routers.customers:router
- plugins.crm.backend.routers.accounts:router
# Path (relative to plugin root) of the dbt workspace. Registered with
# the dbt scheduler when dbt auto-attach lands.
dbt_workspace: dbt/
# Frontend entry point — bundled into the Platform Vite build when frontend
# auto-attach lands. Plugin pages appear under /plugins/<name>/...
frontend_entry: frontend/src/index.tsx
# Routes contributed by the plugin's frontend. Used to populate the
# Platform sidebar's nav.
nav:
- label: Customer 360
path: /crm/customers
icon: users
- label: Accounts
path: /crm/accounts
icon: briefcase
# Permissions the plugin will request. The Platform validates these against
# the catalog at install time.
permissions:
- customer.read
- customer.edit
- account.read
# Default seed groups, rolled into the org's group_permissions on install.
seed_groups:
- name: crm_admin
permissions: [customer.read, customer.edit, account.read]
- name: crm_viewer
permissions: [customer.read, account.read]
Fields are validated at discovery; unknown fields are warnings (forward-compat), required fields missing is an error.
Discovery
from services.plugin_host import discover_plugins
for plugin in discover_plugins():
print(plugin.manifest.name, plugin.manifest.version, plugin.source, len(plugin.manifest.routers))
discover_plugins() returns a list of DiscoveredPlugin records. Each carries the parsed manifest, the source (install or data), the root path, and any per-plugin errors. If a plugin.yaml is malformed the plugin is included with errors set — discovery never raises, so a single bad plugin doesn't take down the platform.
mount_plugins(app) (called from paas/backend/main.py at module-import time) iterates the discovered list, imports each module:attr router, and app.include_routers it. Errored plugins are skipped — see the /api/plugins admin page for status.
What v0.0.34 still doesn't do
Tracked under tech-debt #7:
- Frontend — Vite doesn't yet auto-merge plugin frontend bundles. Today, plugin pages need a manual mount during dev.
- dbt workspace —
dbt_project.ymlpaths are listed in the manifest but not yet registered with the dbt scheduler. - Permissions auto-seed — manifest
permissionsandseed_groupsare validated but not yet written to the catalog at install time. - Lifecycle endpoints —
/api/pluginsis read-only. There's noPOST /api/plugins/install, no enable/disable. Lifecycle is "drop the folder, restart the platform".
Once those land, a vertical-specific install becomes "drop the plugin folder under $DATA_DIR/plugins/ and restart". No separate service, no PaaS-internal imports.
Writing a plugin today
The mount loop works end-to-end as of v0.0.34. The full flow:
- Create the
<DATA_DIR>/plugins/<your-vertical>/directory with the layout above (or<INSTALL_DIR>/plugins/if you're shipping it bundled with the platform). - Write
plugin.yamlagainst the manifest schema. - Validate:
python -c "from services.plugin_host import discover_plugins; [print(p) for p in discover_plugins()]". - Restart
hub-platform.service— your routers mount automatically. - Visit
/pluginsin the Platform UI (Administration menu) to confirm it discovered correctly. Errors show up in the Status column. - If your plugin also needs a frontend or dbt workspace, mount those by hand for now (see "What v0.0.34 still doesn't do").
The bundled _demo plugin under plugins/_demo/ is the canonical reference — minimal manifest + one router, ~30 lines total. Copy it as a starting point.
Plugin packaging (planned)
Long-term, plugins ship as Python wheels with PEP 621 entry points:
[project.entry-points."honeyframe.plugins"]
crm = "plugins.crm:manifest"
That replaces the directory-walking discovery with importlib.metadata.entry_points(). Both forms will be supported during the transition.
Don't extend by editing PaaS internals
If your goal is "add a new connector" or "add a new dashboard block", you do not need a plugin manifest. Both surfaces have type-level extension points — see Connectors and Dashboards for adding to the registry. Plugins are for vertical-shaped extensions: a coordinated bundle of backend routes, frontend pages, dbt models, and permissions for a domain like crm or finance.