Skip to main content

Plugins

Lumio has a plugin system in the formal "trait + registry + metadata" sense, but almost everything it currently registers is built-in and compiled into the API binary. Dog-fooding is the explicit design goal: built-in widgets, bot modules, platforms, and event sources are all registered through the same traits that a third party would use, so the abstraction is exercised on every request. External / user-authored plugins are not yet supported — see Planned work below.

What a plugin is in Lumio

A plugin is any type that implements one of the plugin traits in crates/lo-plugins/src/traits.rs and is registered in the PluginRegistry at API startup. Each registered plugin contributes PluginMetadata (name, display_name, description, version, author, icon, category) that surfaces in the API (GET /v1/plugins) and drives the dashboard UI.

Plugin types

TraitRust locationWhat it describesClient counterpart
WidgetPluginlo-plugins::traits::WidgetPluginOverlay widget (Spotify Now Playing, Chat Box, Alert Box, Event List). Provides default_config + validate_config.WidgetPlugin in @lumio/plugins — the React component, settings panel, default config.
BotModulePluginlo-plugins::traits::BotModulePluginChat moderation module. Wraps an lo_bot_modules::BotModule implementation.(none — runs server-side only)
PlatformPluginlo-plugins::traits::PlatformPluginA streaming platform. Exposes AuthFlow (OAuth / ApiKey / AccessToken) and SetupStep[] for the connection wizard.(used via the connections UI)
EventPluginlo-plugins::traits::EventPluginAn event source. Declares the platform:action event types it can produce.(consumed by the automation builder)
AutomationNodePluginlo-plugins::traits::AutomationNodePluginA custom node in the visual automation builder. Exposes JSON-Schema input_schema and output_schema.AutomationNodePlugin in @lumio/plugins — the config React component.

Registry and discovery

Server side. PluginRegistry (crates/lo-plugins/src/registry.rs) holds one HashMap<String, Box<dyn …>> per plugin type, enforces unique names within a type, and is built at startup by lo_plugins::create_default_registry (crates/lo-plugins/src/builtins/mod.rs). That function registers every built-in:

  • Widgets: SpotifyNowPlayingWidget, ChatBoxWidget, AlertBoxWidget, EventListWidget.
  • Bot modules: LinkProtectionPlugin, SpamProtectionPlugin, WordFilterPlugin, TimedMessagesPlugin.
  • Platforms: TwitchPlatform, YouTubePlatform, KickPlatform, TrovoPlatform, DiscordPlatform.
  • Events: TwitchEvents, YouTubeEvents, KickEvents, TrovoEvents.
  • Automation nodes: none registered by default yet.

The resulting Arc<PluginRegistry> is injected into Actix Web state and exposed through:

  • GET /v1/pluginsPluginListResponse \{ widgets, bot_modules, platforms, events, automation_nodes, total \}.
  • GET /v1/plugins/\{plugin_type\}PluginTypeResponse for a single category (widgets, bot_modules, platforms, events, automation_nodes).

Both endpoints require a bearer token but no special permission — they return only metadata, not configuration.

Client side. @lumio/plugins (shared/plugins/) mirrors the Rust types in TypeScript:

  • PluginRegistry / createPluginRegistry() in shared/plugins/src/registry.ts — manages WidgetPlugin and AutomationNodePlugin instances that carry React components (component, settingsComponent, configComponent). This is what the dashboard / overlay renderer consults when it needs to mount a widget by name.
  • PluginListResponse / PluginTypeResponse types match the API payloads 1:1 (snake_case on the wire, per the project convention).

Today the client registry is constructed in-app (widgets are imported and registered at build time in the web and overlay apps) — there is no runtime download of third-party React bundles.

Permissions a plugin can request

Plugins do not have an independent permission model. They run inside the API's existing RBAC: a bot-module plugin executes under the chat pipeline's account context, a platform plugin's AuthFlow declares OAuth scopes that end up in channel_connections.scopes, and widget plugins render under the overlay page's popout token or JWT. Permission strings follow the standard resource:action format enforced in RBAC & Permissions.

Authoring a plugin (today)

Because Lumio is closed-source and has no dynamic-load path, "authoring a plugin" means adding a new built-in:

  1. Pick the trait. Decide whether you are adding a widget, bot module, platform, event source, or automation node.
  2. Implement the trait in crates/lo-plugins/src/builtins/<category>.rs. Provide metadata() (include a stable name, a semver version, and an icon identifier), plus the category-specific methods (e.g. default_config / validate_config for widgets, bot_module() for bot modules).
  3. Register it in lo_plugins::builtins::create_default_registry so it ships with every API build.
  4. Mirror the client side (widgets / automation nodes only): add a WidgetPlugin or AutomationNodePlugin implementation in the appropriate apps/web or overlay tree, register it with createPluginRegistry(), and make sure the metadata.name matches the Rust side exactly.
  5. Add translations. Any user-facing string (display_name, description, setup-step text) must live in apps/web/messages/\{en,de\}.json and be referenced via next-intl, not hardcoded.
  6. Bump the version in the relevant Cargo.toml / package.json per the project versioning rule.

The same GraphQL ↔ REST parity requirements apply: if your plugin introduces new endpoints or mutations, both protocols ship together.

Local development workflow

  1. just stack-up — starts Postgres / TimescaleDB / Redis.
  2. just dev-api — rebuilds the API on change; create_default_registry re-runs at each restart and you'll see Registered widget plugin <name> / etc. in the info logs, plus a final total = N summary.
  3. just dev-web — the web app picks up newly-imported client widgets on HMR.
  4. Hit GET /v1/plugins to confirm your new plugin is registered.
  5. just lint-all + just test-all — clippy treats warnings as errors, and unit tests around registration live in crates/lo-plugins/src/registry.rs and per-builtin files.

Deployment

Plugins ride along with the main API binary (lumio-api) and the Next.js web / overlay bundles. There is no separate plugin artifact, no plugin-upload endpoint, and no runtime install. To ship a new plugin, release a new version of the monorepo.

Planned work

The following are explicitly not implemented yet. Do not rely on them:

  • Third-party / user-authored plugins. PluginRegistry::register_* is called only at startup by create_default_registry. There is no API, no signed-bundle format, and no sandbox for external code.
  • Dynamic / hot-reloadable plugins. Every change requires a rebuild + redeploy.
  • Permission-scoped plugin contexts. A built-in plugin runs with full API privileges; there is no per-plugin capability token yet.
  • Marketplace / plugin registry service. No discovery, pricing, or review flow exists.

Everything on that list is intentional for now — the trait surface is stable enough for internal dog-fooding, but externalising it needs a sandbox story first. Track follow-up work in the roadmap before designing against it.

Key files

  • crates/lo-plugins/src/lib.rs — crate entry point, re-exports.
  • crates/lo-plugins/src/traits.rs — the five plugin traits.
  • crates/lo-plugins/src/metadata.rsPluginMetadata, PluginCategory.
  • crates/lo-plugins/src/registry.rsPluginRegistry implementation.
  • crates/lo-plugins/src/builtins/mod.rscreate_default_registry bootstrap.
  • apps/api/src/routes/plugins.rsGET /v1/plugins + GET /v1/plugins/\{plugin_type\}.
  • shared/plugins/src/types.ts, registry.ts, index.ts — client-side @lumio/plugins mirror.