Plugin Development Guide
Overview
WPrint 3D plugins are host-controlled extensions packaged as .w3dp archives or mounted live from source in the development stack. Every plugin is described by a single plugin.json manifest and can combine:
a runtime shape:
phporbridgea UI shape:
declarative,webview, orcustom_bundlea dependency footprint:
lightweightorheavyweightone or more surfaces:
settings_tab,navbar_widget,printer_panel,printer_action,modal,page
Use the lightest shape that fits the job. The platform is intentionally biased toward host-rendered UI and short-lived runtime handlers because many WPrint 3D installs run on low-memory SBCs.
Lightweight vs Heavyweight
lightweight
Declares no container images in the manifest.
Installs without extra runtime dependencies.
Best for host-rendered UI, PHP hooks, and small bridge adapters that already exist elsewhere.
heavyweight
Declares one or more images in
plugin.json -> images.WPrint 3D will pull those images during install/update.
Optional healthcheck commands can be executed to prove the image is usable.
Optional
requirements.memoryMbandrequirements.cpuCoreslet the host warn when the system is below the plugin’s minimum target.
This is intentionally close to the host-managed add-on model used by systems like Home Assistant: the plugin declares its container dependencies, but WPrint 3D owns the pull, readiness, and trust UX.
Choose A Shape
Runtime
php
Best default for small plugins.
Runs short-lived PHP handlers inside the core environment.
Good for lightweight actions, hooks, and host-owned UI.
bridge
Use when your plugin already has its own service process or non-PHP stack.
WPrint 3D calls the external service over HTTP.
Good for integrations, heavy processing, or codebases that should stay outside the main PHP runtime.
UI
declarative
Best default.
Host-rendered and theme-native.
Recommended for settings tabs, cards, lists, forms, buttons, and compact metrics.
Supports a stable
host.*component registry plushost.remote_component, a reusable host-rendered component kind that works on web and native.
webview
Isolated HTML surface.
Use when you need custom layout, richer typography, or browser-native APIs.
Prefer asset-backed HTML served by WPrint 3D rather than remote public URLs.
custom_bundle
Elevated UI path for heavier assets or richer client logic.
Same isolation model as WebView in the current host renderer, but treated as a more privileged UI mode.
Use sparingly and document why the declarative path is insufficient.
About The components Field
Manifest-declared components now support two distinct execution models:
remote_componentfor host-rendered declarative UI on web and nativebrowser_modulefor elevated browser-based surfaces
This still does not mean arbitrary runtime-loaded React Native code. In practice:
remote_componentis a manifest-declared template the host resolves into native React Native Paper UIbrowser_moduleis a JS module imported by WebView/custom-bundle HTML
That gives us a real cross-platform remote component API without allowing unbounded third-party React Native code execution inside the app shell.
Host Component Registry
The declarative host renderer now exposes a stable registry of host components. New plugins should prefer host.* namespaced component ids instead of depending on raw renderer internals.
Recommended components:
layout:
host.stack,host.row,host.surface,host.scroll,host.spacertypography:
host.heading,host.text,host.captionactions/forms:
host.button,host.input,host.switch,host.formdisplay:
host.section,host.card,host.list,host.key_value,host.badge,host.chip_group,host.progressdynamic widgets:
host.progress_cluster,host.data_strip,host.remote_component
Legacy ids like section, text, button, progress_cluster, and remote_component still work, but they are compatibility aliases now.
Manifest Anatomy
{
"id": "acme.hello-world",
"name": "Hello World",
"version": "0.1.0",
"sdkVersion": 1,
"sdkRevision": 4,
"homepageUrl": "https://github.com/acme/hello-world-plugin",
"documentationUrl": "https://github.com/acme/hello-world-plugin#readme",
"sourceUrl": "https://github.com/acme/hello-world-plugin",
"runtime": {
"type": "php",
"entry": "plugin.php"
},
"permissions": [
"ui.settings_tab"
],
"actions": [
{
"id": "ping",
"label": "Ping",
"handler": "actions/ping.php"
}
],
"uiExtensions": [
{
"id": "settings",
"surface": "settings_tab",
"mode": "declarative",
"title": "Hello World",
"schema": {
"component": "host.section",
"children": [
{
"component": "host.button",
"label": "Ping",
"actionId": "ping"
}
]
}
}
],
"requirements": {
"memoryMb": 1024,
"cpuCores": 2
},
"images": [
{
"id": "metrics-service",
"image": "ghcr.io/acme/metrics-service:1.2.3",
"engine": "auto",
"healthcheck": {
"command": ["php", "-v"],
"timeoutSecs": 15
}
}
],
"signature": {
"algorithm": "none"
}
}
For public releases, treat homepageUrl, documentationUrl, and sourceUrl as canonical metadata. Registry entries and landing pages should read from those fields instead of guessing repository links from wherever the plugin happened to be developed.
Plugin Translations
Plugins can ship host-managed translations for manifest metadata and declarative UI without bundling a second frontend runtime.
Declare translation JSON files in assets, then map locales under i18n.files:
{
"assets": [
{ "path": "translations/es.json" }
],
"i18n": {
"defaultLocale": "en",
"files": {
"es": "asset://translations/es.json",
"es_AR": "asset://translations/es.json"
}
}
}
Each translation file can override:
plugin.nameplugin.descriptionactions.<actionId>.labeluiExtensions.<extensionId>.titleuiExtensions.<extensionId>.schemacomponents.<componentId>.schema
Example translations/es.json:
{
"plugin": {
"name": "Hola Mundo",
"description": "Plugin de referencia"
},
"actions": {
"ping": {
"label": "Probar"
}
},
"uiExtensions": {
"settings": {
"title": "Configuración",
"schema": {
"title": "Interfaz declarativa liviana"
}
}
}
}
Resolution rules:
The host first applies the plugin’s
i18n.defaultLocale.Then it applies the requested base language such as
es.Finally it applies the exact locale such as
es_AR.Missing keys fall back to the base manifest instead of rendering empty text.
For webview and custom_bundle extensions, WPrint 3D also appends locale and fallbackLocale query params to the embedded URL so browser-side plugin code can load matching translations.
Building A Plugin
1. Scaffold It
./plugin.sh make
The scaffold is now interactive by default. It prompts for:
plugin identifier and display name
shape:
php/bridgeplusdeclarative/webview/custom_bundleoptional required image reference
optional managed bridge service wiring
optional minimum memory and CPU targets
You can still use it non-interactively:
./plugin.sh make acme.hello-world "Hello World" --shape=bridge-custom-bundle --image=ghcr.io/acme/hello-world-service:latest --memory=1024 --cpu=2
The scaffold emits the current SDK pair:
sdkVersionsdkRevision
It also creates a local AGENTS.md file inside the plugin directory so contributors have a plugin-scoped working guide next to the manifest, runtime files, and assets.
Use ./plugin.sh from the host checkout when possible. It enters the running backend container and executes php artisan plugin:* there, which avoids requiring the host machine to have the same PHP runtime as the stack.
Scaffolds created with ./plugin.sh make land in repo plugins by default. In the development stack, the unpacked install flow discovers both your local plugins/ directory and the bundled reference plugins under examples/plugins, so new plugins show up next to the samples in Settings -> Plugins -> Add a plugin -> Install unpacked.
Runtime Diagnostics
Every installed plugin now keeps a bounded lifecycle log and a host-visible load state.
In the UI, the plugin inventory exposes:
a
Logsbutton on every installed plugin carda
Failed to loadbadge when startup or source sync failedthe most recent startup error in the card body
Use this when developing:
install or refresh the plugin
enable it
if it fails, open
Logsfix the startup issue and try again
This is the intended debugging path. Plugins should fail gracefully and leave diagnostics behind instead of taking the host down with them.
Porting OctoPrint Plugins
The current SDK revision is designed to make OctoPrint ports mechanically straightforward.
Use this mapping:
SettingsPlugin.get_settings_defaults()->settings.defaults_plugin_manager.send_plugin_message(...)->send_plugin_messageTemplatePluginsettings template ->settings_tabnavbar template ->
navbar_widgetAssetPluginfiles -> manifestassetsbrowser AJAX/view model code ->
/api/plugins/sdk/octoprint-compat.js
Recommended Port Strategy
Keep the original settings keys.
Rebuild navbar/status widgets as host-rendered declarative surfaces first.
Use a
custom_bundlesettings tab only if the original browser-side behavior is meaningful and reusable.If the original plugin only needed periodic refresh while visible, use action polling instead of recreating a long-lived server timer.
OctoPrint Browser Helper
For browser-based settings pages, import:
await import("/backend/api/plugins/sdk/octoprint-compat.js");
const host = window.WPrint3DOctoPrintCompat.fromWindow();
Then use:
host.getSettings()host.saveSettings(settings)host.getState()host.watchState(callback, options)host.invokeAction("actionId", payload)
Reference Port
Use examples/plugins/octoprint-navbartemp-port as the reference implementation.
It shows:
persisted settings defaults that mirror the original OctoPrint plugin
a host-rendered native navbar strip
a
custom_bundlesettings tab using the compatibility helpersend_plugin_messagestate updates that keep the navbar and the settings preview in sync
Host-rendered settings_tab pages are mounted inside a shared WPrint 3D shell. That shell keeps the plugin title visible at the top, but collapses the heavier metadata/warning hero by default so the plugin’s own settings UI remains the primary thing on screen.
Shape values
php-declarativephp-webviewphp-custom-bundlebridge-declarativebridge-webviewbridge-custom-bundle
2. Pick The Runtime
For php:
"runtime": {
"type": "php",
"entry": "plugin.php"
}
For bridge:
"runtime": {
"type": "bridge",
"baseUrl": "http://bridge-service:9310"
}
Or, for a host-managed bridge image:
"runtime": {
"type": "bridge",
"managedImageId": "metrics-service",
"healthcheck": "/health"
}
Bridge plugins should expose:
GET /healthaction endpoints such as
POST /actions/host_metricsoptional hook endpoints such as
POST /hooks/app.boot
If runtime.managedImageId is used, the referenced image must declare service.port, and WPrint 3D will:
pull the image during install/update
start the container when the plugin is enabled
attach it to the current WPrint 3D container network
resolve the bridge
baseUrlautomatically from the managed service alias and port
3. Add Actions
Actions are the typed entrypoints the host can invoke from:
declarative buttons/forms
polling widgets like
progress_clusterWebView/custom-bundle pages that call back through the host API
PHP action example:
<?php
$input = json_decode(stream_get_contents(STDIN), true);
echo json_encode([
'data' => [
'message' => 'pong',
'payload' => $input['payload'] ?? [],
],
]);
4. Add Settings UI
If a plugin declares a settings_tab, WPrint 3D:
creates a dedicated tab inside Settings
shows the plugin icon with the puzzle badge
adds a
Settingsbutton to the plugin card
That keeps the plugin inventory clean and avoids embedding arbitrary settings UI inside the plugin list.
5. Package Or Mount
Unsigned development build:
./plugin.sh pack examples/plugins/host-metrics
That writes the archive to:
examples/plugins/host-metrics/builds/host-metrics.w3dp
Generate or reuse a long-lived signing key before public releases:
./plugin.sh keygen --output ../plugin-signing/host-metrics.pem
Interactive signed release flow:
./plugin.sh pack examples/plugins/host-metrics --wizard
Non-interactive signed release flow:
./plugin.sh pack examples/plugins/host-metrics \
--signing-key ../plugin-signing/host-metrics.pem
Every signed package embeds the signer public key in plugin.json -> signature.publicKey.
Verify before installing or publishing:
./plugin.sh verify examples/plugins/host-metrics/builds/host-metrics.w3dp
./plugin.sh verify examples/plugins/host-metrics/builds/host-metrics.w3dp --require-trusted
Restore a source tree from a package when you need to test or fork a published release:
./plugin.sh restore examples/plugins/host-metrics/builds/host-metrics.w3dp \
--output plugins/host-metrics-fork
Or, in development mode:
run
./run.sh -e devenable
developerModeopen
Settings -> Plugins -> Add a plugin -> Install unpackedinstall the source directory directly from the mounted development path
Container Images And Resource Requirements
Heavyweight plugins declare image dependencies in the manifest:
"requirements": {
"memoryMb": 1024,
"cpuCores": 2
},
"images": [
{
"id": "metrics-service",
"image": "ghcr.io/acme/metrics-service:1.2.3",
"engine": "auto",
"healthcheck": {
"command": ["curl", "-f", "http://127.0.0.1:9310/health"],
"timeoutSecs": 15
},
"service": {
"port": 9310,
"networkAlias": "acme-metrics"
}
}
]
Notes:
imagesis optional. No images means the plugin islightweight.enginecan beautoordocker.healthcheck.commandis optional but recommended for heavyweight plugins.requirementsis optional. If omitted, WPrint 3D will still install the plugin and try to run it.If requirements are declared and the host falls short, install still succeeds, but the UI shows warnings so the user can make an informed decision.
Managed bridge services
If a bridge plugin declares runtime.managedImageId, the matching image entry must also declare service.port.
WPrint 3D uses that to create a host-managed sidecar container named after the plugin and image ID. This is the recommended way to ship self-contained bridge plugins with their own API server or large runtime dependencies.
Asset-Backed Elevated UI
For webview and custom_bundle, declare assets in the manifest and reference them with asset://.
"uiExtensions": [
{
"id": "settings",
"surface": "settings_tab",
"mode": "webview",
"title": "Host Metrics",
"url": "asset://ui/settings.html",
"dataActionId": "host_metrics"
}
],
"assets": [
{
"path": "ui/settings.html"
}
]
WPrint 3D rewrites asset://ui/settings.html into an authenticated same-origin asset URL at runtime.
You can also declare JS component modules in the manifest:
"components": [
{
"id": "hostMetricsCard",
"kind": "browser_module",
"entry": "asset://components/host-metrics-card.js",
"exports": "mount"
}
]
Or declare a reusable host-rendered remote component:
"components": [
{
"id": "hostMetricsPanel",
"kind": "remote_component",
"schema": {
"component": "host.section",
"title": {
"$prop": "title",
"default": "Host telemetry"
},
"children": [
{
"component": "host.text",
"text": "{{description}}"
}
]
}
}
]
And consume it from a declarative schema:
"schema": {
"component": "host.remote_component",
"componentId": "hostMetricsPanel",
"props": {
"title": "Host telemetry",
"description": "Rendered by the host on web and native."
}
}
And reference them from the elevated UI extension:
"uiExtensions": [
{
"id": "host-metrics-settings",
"surface": "settings_tab",
"mode": "custom_bundle",
"title": "Host Metrics",
"bundle": {
"url": "asset://ui/settings.html"
},
"components": ["hostMetricsCard"]
}
]
The host also appends runtime metadata to the elevated UI URL:
pluginIdpluginNameextensionIdextensionModepluginApiBaseactionIdwhen declared on the extensioncomponentscomponentIdsthemeas JSON theme tokens
That lets the page inherit the active theme and call back into the host API without hardcoding instance-specific URLs.
Shape Matrix Examples
Use the Host Metrics matrix as the reference implementation:
These samples deliberately keep the feature set the same:
CPU and RAM telemetry
navbar widget
dedicated settings tab
Only the runtime/UI shape changes.
The custom-bundle variants additionally demonstrate manifest-declared JS component modules:
The declarative variants demonstrate the cross-platform remote component API:
Best Practices
Default to
php + declarativeunless you can explain why you need something heavier.Prefer
host.*declarative component ids for new plugins so the manifest stays aligned with the documented registry.Keep actions fast and idempotent.
Use dedicated
settings_tabpages instead of crowding the plugin inventory.Declare every WebView/custom-bundle entry asset explicitly.
Pass state through actions and host APIs, not ad-hoc remote globals.
Treat bridge services as production dependencies: healthcheck them, version them, and document how they are deployed.
Keep plugin IDs stable and dotted, for example
acme.host-metrics.Request only the permissions your plugin actually uses.
Add screenshots and an E2E script when shipping a public sample plugin.
Development Workflow
Scaffold or copy from the closest shape example.
Implement actions and hooks.
Add the correct UI surface.
Package or install unpacked from the development mount.
Verify in browser and, if relevant, through CLI flows.
Add docs and screenshots before publishing.
Publishing Workflow
Increment plugin version.
Confirm the manifest targets a supported
sdkVersionandsdkRevision.Package the plugin.
Sign it before any public release.
Back up the private key in at least one secure encrypted location.
If you lose the private key, restore that PEM from backup, run
chmod 600 /path/to/key.pem, and regenerate the public key withopenssl pkey -in /path/to/key.pem -pubout -out /path/to/key.pub.pemif needed.If you want official-registry inclusion, keep the plugin in its own repository and open a PR against the public registry with that repository URL plus the signed package details.
Wait for the WPrint 3D team to reach out before expecting that plugin to appear publicly.
Otherwise, publish it through your own trusted registry or direct
.w3dpdistribution.Add release notes that mention the SDK revision, runtime/UI shape, and signer continuity if you rotated keys.
Source Checkout Requirement
Today, plugin development is anchored to the full wprint3d-core source tree.
That repository is intentionally small, and the supported workflow depends on the monorepo backend container for:
./plugin.shlive development mounts
plugin packaging
plugin signing
browser E2E validation against the running stack
If you want to develop plugins, clone the full source tree first and work from that checkout instead of trying to package from a standalone extracted plugin directory.
Signing Guides
Use these guides together:
Plugin authors: docs/plugin-signing-for-developers.md
Public registry maintainers: docs/plugin-registry-signing-review.md