hubra plugins

Marketplace › Build a plugin

Build a plugin for Hubra.

A plugin adds features to Hubra without changing Hubra itself: its own tables, an API, screens in the club app and settings. This page goes from the template to a signed release.

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.

  1. On GitHub, choose Use this template and name the repository hubra-plugin-<id>.
  2. 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.
  3. Replace the example migration with your own, set version: "1.0.0" and write the first CHANGELOG.md entry.
  4. 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:

  1. Bump version in plugin.yaml and add a ## <version> section to CHANGELOG.md.
  2. Merge, then publish a GitHub release tagged v<version>.
  3. The workflow tests it, builds the frontend, signs it with the Hubra plugin key after approval, and attaches <id>-<version>.zip with its checksum.
  4. 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

To the marketplace