Plugins
Honeyframe's plugin architecture is scaffolded but not feature-complete as of v0.0.x. The contract (manifest format, discovery routine) is in place; runtime mounting (router auto-attach, frontend asset merging) is the next step.
This page documents what exists today, what the runtime model will look like when the scaffold matures, and how to write a manifest now so your code is ready to plug in.
Today's state
The plugin host is paas/backend/services/plugin_host.py. It defines:
- A typed
PluginManifestdataclass — the contract. - A
discover()routine that walks<INSTALL_DIR>/plugins/<vertical>/and returns validated manifests.
What it does not do yet:
- Mount FastAPI routers from plugins. (Today, vertical apps run as separate sibling apps — each one is its own service that imports PaaS routers and adds vertical-specific ones.)
- Merge plugin frontend bundles into the main Vite build.
- Register plugin dbt workspaces with the dbt scheduler.
When those land, plugins/ becomes the canonical extension surface — until then, vertical features go in their own service (saas/) and import PaaS internals.
Plugin layout
A plugin lives at <INSTALL_DIR>/plugins/<vertical-slug>/ with this layout:
plugins/healthcare/
├── plugin.yaml ← required manifest
├── backend/
│ ├── routers/
│ │ ├── patients.py ← FastAPI router
│ │ └── doctors.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 (healthcare, finance, etc.) — lowercase, hyphenated, must match plugin.yaml's name field.
plugin.yaml
The manifest contract:
name: healthcare # vertical slug, required
version: 0.1.0 # plugin semver, versions independently of PaaS
display_name: Healthcare # 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.healthcare.backend.routers.patients:router
- plugins.healthcare.backend.routers.doctors: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: Patient 360
path: /healthcare/patients
icon: users
- label: Doctors
path: /healthcare/doctors
icon: stethoscope
# Permissions the plugin will request. The Platform validates these against
# the catalog at install time.
permissions:
- patient.read
- patient.edit
- doctor.read
# Default seed groups, rolled into the org's group_permissions on install.
seed_groups:
- name: clinical_admin
permissions: [patient.read, patient.edit, doctor.read]
- name: clinical_viewer
permissions: [patient.read, doctor.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
for manifest in discover():
print(manifest.name, manifest.version, len(manifest.routers))
discover() returns a list of PluginManifest instances, validated against the schema. If a plugin.yaml is malformed, the plugin is skipped and a warning is logged — discovery never raises.
The discovery root is <INSTALL_DIR>/plugins/. INSTALL_DIR is set by the systemd unit; in dev mode without systemd, plugin_host falls back to the repo root.
When auto-attach lands
The next step (tracked under tech-debt #7) is to use the manifest to actually mount the plugin:
- Backend routers — at
app.startup, iteratediscover()andapp.include_router(import_module(path))for every router in every manifest. - dbt workspace — register the plugin's
dbt_project.ymlwith the dbt scheduler so its models build alongside PaaS models. - Frontend — Vite picks up plugin entry points via a generated
pluginsManifest.tslisting each enabled plugin's bundle. The Platform shell loads them as code-split chunks. - Permissions — install-time, the manifest's
permissionslist is added to the catalog and theseed_groupsare written intogroup_permissions.
Once that ships, a vertical-specific install becomes "drop the plugin folder under plugins/ and restart the platform". No separate service, no monorepo branch, no PaaS-internal imports.
Writing a plugin today
Even without runtime auto-attach, you can:
- Create the
plugins/<your-vertical>/directory with the layout above. - Write
plugin.yamlagainst the manifest schema. - Run
python -c "from services.plugin_host import discover; print(discover())"to validate. - Develop your routers and UI as if they were already mounted (in dev, mount them by hand in
paas/backend/main.pyfor now). - When auto-attach ships, the manual mount is removed and the same code keeps working.
The reference vertical app under saas/ is the de-facto reference plugin — when extraction lands, vertical services move under plugins/<vertical-slug>/ with no functional change.
Plugin packaging (planned)
Long-term, plugins ship as Python wheels with PEP 621 entry points:
[project.entry-points."honeyframe.plugins"]
healthcare = "plugins.healthcare: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 healthcare or finance.