What a plugin can do
A plugin is a Python package with a small React frontend. It brings one or more modules, just like Hubra's built-in modules:
- Backend: API routes with permissions, its own tables in every club, settings, navigation entries, event hooks and Celery tasks.
- Frontend: its own screens, tabs on built-in screens and cards in fixed places (slots) of the club app, translated into German and English.
- Versions: every version is signed, carries its changes and can be rolled back.
Platform admins install a plugin once from the marketplace; then each club switches its modules on or off under Settings › Modules & plugins.
A plugin runs with full access to the data of every club on a Hubra. So a Hubra installs only signed plugins, and on hubra.club we review every plugin before we install it.
Start from the template
The hubra-plugins-template is a small working plugin (a notice board) with every part: a table, an API, a screen, a setting, a migration, a test, and the workflow that tests, signs and releases it.
- On GitHub, choose Use this template and name the repository
hubra-plugin-<id>. - Rename everything called
example: plugin id, package (hubra_<name>), module code, tables, API path, route and translations. The template's README has the full table. - Replace the example migration with your own, set
version: "1.0.0"and write the firstCHANGELOG.mdentry. - Import the rulesets from
.github/rulesets/into the repository and turn on secret scanning.
Layout
plugin.yaml # id, version, Hubra range, package, modules
CHANGELOG.md # "## 1.1.0" sections, shown before each update
.releaseignore # kept out of the release zip
hubra_example/
__init__.py # module = Module(...)
models.py # SQLAlchemy models (one copy per club)
routes.py # the API, with permission_required(...)
migrations/ # Alembic revisions of this plugin only
frontend/src/
index.tsx # registerPlugin({...})
translations.json # en-GB and de-CH
tests/ # runs with Hubra's pytest fixtures
.github/workflows/release.yml
plugin.yaml describes the plugin. Its name, description and tags appear in the marketplace as written:
id: example
name: Example
version: "1.0.0"
hubra: ">=0.2,<1" # Hubra versions it works with
description: "One line: what it does"
author: Hubra
homepage: https://github.com/hubra-app/hubra-plugin-example
license: AGPL-3.0-or-later
tags: [inventory]
package: hubra_example # must start with hubra_
modules: [example]
requires: [members] # modules or plugins it needs
migrations: migrations
frontend: frontend/dist/plugin.js
Backend
The package defines Module objects like the built-in modules: blueprints, navigation, settings, permissions and tables. Module codes must not clash with Hubra's, and table names start with the plugin id.
from hubra.core.modules import Blueprint, Module, NavItem, SettingField
module = Module(
code="example",
name="Example",
description="A notice board: short notes for the whole club",
default_enabled=False,
depends_on=("members",),
blueprints=(Blueprint("hubra_example.routes", "example_bp", "/api/v1/example"),),
nav=(NavItem("/app/example", "shell.nav.example", "note",
section="more", perms=("example.notes.read",)),),
settings=(SettingField("max_notes", "int", default=50,
label="Most notes on the board"),),
tables=("example_notes",),
)
The blueprint name sets the permission prefix: example_bp gives example.notes.read. Write migrations with flask plugins revision; they run in every club, with a version table of their own per plugin.
Frontend
frontend/src/index.tsx registers what the plugin shows in the club app. Everything appears only while its module is on for the club and the person has the permission.
import { registerPlugin } from '@hubra/plugin-sdk'
import { IconNote } from '@tabler/icons-react'
import { NotesScreen } from './NotesScreen'
import translations from './translations.json'
registerPlugin({
id: 'example',
routes: [{ path: 'example', module: 'example', perm: 'example.notes.read', component: NotesScreen }],
icons: { note: IconNote },
translations, // { "en-GB": {...}, "de-CH": {...} }
})
Other places: tabs (a tab on a built-in screen) and slots (account.cards, inventory.item, settings.module.actions). React, Mantine, react-query, react-router and react-i18next come from the app at runtime; everything else, such as icons, is bundled.
Develop and test locally
Clone the plugin into plugins/<id> of a Hubra checkout. With HUBRA_PLUGINS_AUTOINSTALL=<id> a local Hubra loads the plugin straight from the folder, without installing it.
cd hubra/app/backend
pytest ../../plugins/<id> # tests, with Hubra's fixtures
flask plugins revision <id> -m "..." --autogenerate # a new migration
cd ../frontend
npm run typecheck:plugins
npm run build:plugin -- ../../plugins/<id> # --watch while you work
The workflow in the plugin repository does the same on every push, against Hubra's current main.
Publish
In the official marketplace
Each official plugin lives in a public repository hubra-app/hubra-plugin-<id>. To release:
- Bump
versioninplugin.yamland add a## <version>section toCHANGELOG.md. - Merge, then publish a GitHub release tagged
v<version>. - The workflow tests it, builds the frontend, signs it with the Hubra plugin key after approval, and attaches
<id>-<version>.zipwith its checksum. - This marketplace picks up the release within the hour, checks signature and checksum, signs the index again and lists the version here.
Built a plugin that should be in the official marketplace? Write to us at [email protected].
In a marketplace of your own
Every Hubra can add more repositories. With keys of your own you sign the plugins and the index yourself, and put index.json on any web server, GitHub Pages for example:
flask plugins keygen index.pem # once: the index key
flask plugins keygen plugins.pem # once: the plugin key
flask plugins repo ./plugins ./public --key index.pem --plugin-key plugins.pem --name "My plugins"
flask plugins check https://me.github.io/plugins/index.json --key-id <key id>
Platform admins then add the URL and the key id under Plugin catalogue › Repositories. Your plugins show up there as “Community”.
Rules that apply
- Migrations stay backwards compatible. A rollback resets the version, not the database. Add columns and tables, and remove them only a release later.
- Names of your own. Package starting with
hubra_, tables prefixed with the plugin id, module codes that don't clash with Hubra's. - One key, one purpose. Plugin keys sign plugins, index keys only the index. Private keys belong in a GitHub environment, never in a repository or organisation secret.
- Release tags are final. A broken version is not replaced; it is withdrawn and followed by a new version.
- Vulnerabilities are reported privately through the security page of the repository.
Read more
- plugins/README.md: everything about plugins, for admins and authors.
- hubra/modules/README.md: how modules work.
- hubra-plugin-keys: a bigger, real plugin to compare with.
- hubra-plugins: how this marketplace is built and signed.