Skip to main content
Version: v3

Loading a Dynamic Plugin

Dynamic plugins can be loaded by VeeCode DevPortal at start time. They are usually published to an OCI registry, and the DevPortal instance loads them from there according to the configuration. During development you load a local export instead, without publishing anything.

Load a local export in devportal-local​

For a workspace export, run yarn dev:dynamic from the workspace root, with the directory of each package added with yarn new as an argument, and follow Creating Your Own Plugin. The helper uses @red-hat-developer-hub/cli (rhdh-cli), writes the operator file dynamic-plugins-root-dev/dynamic-plugins.local.yaml and Compose override dynamic-plugins-root-dev/docker-compose.dynamic-plugins-root.local.yml, and prints the command that uses them. Without the helper, create those two files as shown in the manual fallback on that page. The devportal-local checkout supplies docker-compose.yml and docker-compose.dynamic-plugins-root.yml.

The operator file points to ./dynamic-plugins-src/<exported-directory>. The Compose override mounts dynamic-plugins-src-dev at /opt/app-root/src/dynamic-plugins-src. DevPortal 3.1 does not include /opt/app-root/src/dynamic-plugins/, so a bare ./dynamic-plugins/dist/... entry is skipped.

Plugin entries​

On the local stack, customer plugins are added through an operator plugin file with a top-level plugins: list. The file starts with the default plugin index under includes:. Use a local path for a workspace export or a digest-pinned OCI reference for a published package. See Configure dynamic plugins for the local stack for published entries and Creating Your Own Plugin for local exports.

A published entry has this shape:

plugins:
- package: oci://quay.io/veecode/<plugin-name>@sha256:<digest>
disabled: false

An image that holds one plugin needs no !<plugin path> suffix: the installer reads the path from the image manifest. To turn on or change a plugin that is already in the default plugin list, follow Adding Plugins instead.

Generating the integrity hash​

The integrity: field is required for remote npm packages. The installer downloads the package, recomputes its SHA-512, and refuses to load it on mismatch.

It is not used for:

  • OCI packages (oci://...) — these are validated by digest comparison.
  • Local exports (./dynamic-plugins-src/...) use the Compose source mount and do not need an integrity value. The mount supplies the package directory that the 3.1 image does not bundle.

Expected format: sha512-<base64>. Two ways to generate it:

Method A — query the npm registry directly (preferred for public packages):

npm view <package>@<version> dist.integrity
# example
npm view @backstage/plugin-catalog@2.0.5 dist.integrity

Returns the hash in exactly the format the installer compares against. Always pin the version — npm view <package> dist.integrity (no version) returns the latest, which will mismatch if you have a specific version pinned in your package: field.

Method B — compute it locally (fallback for private registries or when npm view fails):

npm pack <package>@<version> && \
HASH=$(cat <package>-<version>.tgz | openssl dgst -sha512 -binary | openssl base64 -A) && \
echo "sha512-$HASH"

This replicates the exact pipeline the installer uses internally (cat archive | openssl dgst -sha512 -binary | openssl base64 -A), so the produced hash is guaranteed to match a successful install.

Private npm registry​

Due to security and compliance reasons you may not want VeeCode DevPortal to load plugins from public npm registries. You may prefer to use a private npm registry, like Nexus or Artifactory.

On Kubernetes, the chart mounts an optional Secret named <release>-dynamic-plugins-npmrc into the plugin installer. Put your .npmrc under the key .npmrc before you add entries that point at the private registry; the installer uses it as its npm configuration through NPM_CONFIG_USERCONFIG.

Wiring plugins​

Dynamic plugins wire themselves to the DevPortal instance through configuration. Unlike static plugins, they cannot modify the host Backstage project's code — wiring happens at runtime, declared in YAML.

The wiring rule depends on the plugin's role, declared in its package.json under backstage.role:

RolepluginConfig needed?Where does its config go?
frontend-pluginYes — describes mount points, tabs, routesInside pluginConfig.dynamicPlugins.frontend.<plugin-id>
backend-pluginNo — auto-discovered by the loaderPlain top-level keys in app configuration (e.g., sonarqube:)
backend-plugin-moduleNo — auto-attaches to its parent pluginPlain top-level keys in app configuration (parent plugin's config)

There is no dynamicPlugins.backend.* key. Backend plugins never wire themselves through pluginConfig — the loader detects them by their package role and registers them automatically.

Backend plugin example (no pluginConfig)​

plugins:
- package: oci://quay.io/veecode/backstage-community-plugin-sonarqube-backend@sha256:2ad11a712d8c7183334efff1c72185c683971702067fa357966c339ce644b7ad
disabled: false
# No pluginConfig — backend role is auto-discovered

The plugin's runtime configuration goes in your regular app configuration as a normal top-level section:

sonarqube:
baseUrl: https://sonarqube.example.com

Same pattern for backend-plugin-module (e.g., a scaffolder action module attaches itself to the scaffolder plugin):

plugins:
- package: oci://quay.io/veecode/backstage-community-plugin-scaffolder-backend-module-sonarqube@sha256:eeab43bc491b7726b513df6bef9c4b88e2c06679df75feca9d4b5557fb950539
disabled: false
# No pluginConfig — module auto-attaches to the scaffolder backend

Frontend plugin example (pluginConfig required)​

Frontend plugins must declare where their UI components mount, because Backstage's frontend has no auto-discovery for routes, sidebars, tabs, or cards:

plugins:
- package: oci://quay.io/veecode/backstage-community-plugin-sonarqube@sha256:a5eeb1c0634799cde872ff9619d7ea80512288bc964d054c6e91cc45014817af
disabled: false
pluginConfig:
dynamicPlugins:
frontend:
backstage-community.plugin-sonarqube:
mountPoints:
- mountPoint: entity.page.overview/cards
importName: EntitySonarQubeCard
config:
if:
allOf:
- isSonarQubeAvailable

The plugin-id under dynamicPlugins.frontend.<plugin-id> is the npm package name with @ removed and / replaced by . (note: ., not -, for this key specifically — different from the OCI reference convention).

See Wiring a Frontend Plugin for the full list of frontend mount points, tab paths, and the scalprum mechanism that processes these declarations at runtime.

Tips​

Get a guest token, then check the loaded plugins:

TOKEN=$(curl -s http://localhost:7007/api/auth/guest/refresh -H 'X-Requested-With: XMLHttpRequest' | python3 -c 'import sys,json; print(json.load(sys.stdin)["backstageIdentity"]["token"])')
curl -H "Authorization: Bearer $TOKEN" http://localhost:7007/api/dynamic-plugins-info/loaded-plugins

This works while guest sign-in is on, which is the local stack default.