Plugin Development Guide¶
This guide explains how to build plugins for Lyndrix Core and how the core runtime interacts with them.
Plugin model¶
A Lyndrix plugin is a Python package inside /app/plugins/<plugin_folder> with an entrypoint.py module.
At minimum, a plugin provides:
- a
manifest - a
setup(ctx)function
The manifest tells Lyndrix what the plugin is, what permissions it needs, and how it should behave during activation.
Directory structure¶
A typical plugin looks like this:
app/plugins/my_plugin/
├── entrypoint.py
├── requirements.txt # optional
├── vendor/ # generated during install, optional in source repos
├── assets/ # optional static assets
└── locales/ # optional translation files, auto-registered
Rules and conventions:
- the folder name must be a valid Python identifier
- if a repository name contains
-, Lyndrix normalizes it to_for imports requirements.txtis supported, but dependencies are installed into the plugin-localvendor/directory
Stable plugin API¶
Plugin code should import from core.api rather than from internal core modules.
Current stable API version:
__api_version__ = "1.0.0"
The goal is to give plugin authors a stable surface even when the internal core structure evolves.
Manifest fields¶
ModuleManifest supports the following important fields:
id— required unique ID, typicallylyndrix.plugin.<name>name— display nameversion— plugin version stringdescription,author,icon— metadata for UI and marketplace viewstype— usePLUGINfor normal pluginsui_route— route mounted by the plugin in the UIpermissions.subscribe— topics the plugin may subscribe topermissions.emit— topics the plugin may emitpermissions.vault_paths— additional Vault paths if neededsettings_schema— optional plugin-defined settings metadatadependencies— list of required modules or pluginsmin_core_version— minimum supported Lyndrix API/core versionauto_enable_on_install— whether the plugin should activate automaticallyrepo_url— source repository URL used for updates and marketplace metadata
Lifecycle hooks¶
Required hook¶
setup(ctx)
setup(ctx) can be synchronous or asynchronous. Lyndrix calls it when the plugin becomes active.
Optional hooks¶
teardown(ctx)— cleanup during deactivation or unloadrender_settings_ui(ctx)— render plugin settings inside the platform settings UIrender_dashboard_widget(ctx)— render dashboard widgets on the main dashboard
Runtime states¶
Plugins can move through several internal states:
initializingactivedisabledblocked
blocked is used when the plugin is enabled but a declared dependency is not available or not active yet.
ModuleContext¶
Each plugin receives a ModuleContext instance named ctx. This is the supported bridge into the core runtime.
Important members include:
ctx.manifest— validated manifest objectctx.log— plugin-specific loggerctx.state— transient in-memory state dictionaryctx.subscribe(topic)— permission-checked event subscription decoratorctx.emit(topic, payload)— permission-checked event emissionctx.create_task(coro, name=...)— tracked async task creationctx.get_secret(key)— read from the plugin Vault namespacectx.set_secret(key, value)— write into the plugin Vault namespace
Vault isolation¶
Secrets are separated by module identity:
- core components use
core/<manifest.id> - plugins use
plugins/<manifest.id> - both are stored inside the Vault mount
lyndrixusing KV v2
This means plugins do not automatically share secret space with each other.
Event integration¶
Plugins should communicate through the event bus via ctx instead of importing the global bus directly.
Common topics worth knowing include:
vault:ready_for_datasystem:boot_completedb:connectedplugin:install_startedplugin:installedplugin:install_failedplugin:files_changedgit:status_updateui:needs_refresh
Important rules:
- every subscribed topic must be declared in
permissions.subscribe - every emitted topic must be declared in
permissions.emit - unauthorized event access is blocked and logged by Lyndrix
Dependencies inside a plugin¶
If a plugin contains a requirements.txt, Lyndrix installs those dependencies during plugin installation or upgrade into the plugin-local vendor/ folder.
This design keeps plugin dependencies isolated from the core application environment.
Notes:
- installation happens before the plugin is moved into its final runtime directory
- install failures abort the operation and trigger cleanup
- suspicious requirement lines such as unsafe local-path style entries are rejected
Installation and versioning¶
Lyndrix can install plugins directly from GitHub repositories.
Supported flows:
- install the default branch as
latest - install a specific tag such as
v1.2.3 - upgrade an existing plugin through an atomic staging and swap workflow
During installation, Lyndrix:
- resolves repository metadata through the GitHub API
- downloads a branch or tag archive
- extracts the archive into a staging directory
- validates extraction paths to prevent ZIP path traversal
- installs dependencies into
vendor/if needed - moves the plugin into
/app/plugins - emits plugin lifecycle events
Desired plugin state¶
LYNDRIX_PLUGINS_DESIRED allows operators to define plugins that should exist after reconciliation.
Format:
Rules:
- omit
@versionto meanlatest - set
LYNDRIX_PLUGINS_AUTO_UPDATE=trueto auto-updatelatestplugins during reconcile
Authentication extensions¶
Plugins can register custom authentication providers at runtime through the event bus.
Relevant event:
auth:register_providerwith payload{"provider": <AuthProvider instance>}
If you do this, the provider ID must also be listed in LYNDRIX_AUTH_PROVIDERS so the provider chain can activate it.
Minimal example¶
from core.api import ModuleManifest
from nicegui import ui
manifest = ModuleManifest(
id="lyndrix.plugin.hello",
name="Hello Plugin",
version="1.0.0",
description="Example plugin",
author="Example",
type="PLUGIN",
ui_route="/hello",
permissions={"subscribe": ["system:boot_complete"], "emit": []},
)
async def setup(ctx):
@ui.page('/hello')
async def hello_page():
ui.label('Hello from plugin')
Best practices¶
- import from
core.apiwhenever possible - keep
manifest.idstable across releases - use
ctx.get_secretandctx.set_secretinstead of plain files or env vars - move long-running work into
ctx.create_task(...) - declare
min_core_versionwhen your plugin depends on newer core behavior - document plugin behavior in the plugin repository itself
Troubleshooting¶
Plugin does not load¶
Check:
entrypoint.pyexistsmanifestis present and valid- the folder name is import-safe
- the plugin import does not fail due to missing dependencies
Plugin is blocked¶
Check:
- whether all dependencies in
dependenciesare installed and active - whether the dependent plugins use the expected
manifest.id
Secret access returns nothing¶
Check:
- whether Vault is open
- whether the plugin waited for
vault:ready_for_data - whether the secret was written under the plugin's own namespace
Upgrade fails¶
Check:
- whether the requested tag exists
- whether the downloaded archive is valid
- whether
requirements.txtcan be installed cleanly