Skip to main content

Installing external plugins

Beyond the built-in and must-use plugins shipped with the admin, you can install third-party plugins packaged as ZIP files. They load at runtime via dynamic import() — no admin rebuild required.

What an external plugin looks like

A plugin ZIP contains:

my-plugin.zip
├── manifest.json — metadata (id, version, apiVersion, entry path)
├── bundle.js — pre-built ESM module that default-exports the PluginManifest
└── (anything else) — assets the bundle references

Authoring guides for plugin developers live at:

For installing only, you just need the .zip.

Step 1 — Get the ZIP

External plugins come from a few places:

  • The plugin author's release page (GitHub Releases, marketplace download, etc.)
  • Building from source if you have access to the plugin's repo (npm run build produces a ZIP under dist/)
  • Internal corporate plugins distributed by your team

Verify the source — ZIPs run with full admin privileges (network requests, Firestore writes, file uploads) so only install plugins you trust.

Step 2 — Click Install plugin

In the admin: PluginsInstall plugin button (top right) → ZIP picker.

The modal shows:

  • Currently installed externals — list of already-installed external plugins, each with an Uninstall button
  • Bundled defaults available — links to re-install built-in plugins that were uninstalled (if any)
  • File picker — drag-and-drop a .zip or click to select

After picking the file:

  1. Client-side ZIP extraction via JSZip. macOS-style wrapping folders (__MACOSX/, single top-level dir) are auto-flattened.
  2. Validation:
    • manifest.json exists and parses
    • manifest.id is sanitised (lower-case ASCII + dashes)
    • manifest.apiVersion falls within the admin's [FLEXWEG_API_MIN_VERSION, FLEXWEG_API_VERSION] range
    • bundle.js exists at the path declared in manifest.entry
  3. Upload every file under /admin/plugins/<id>/ via flexwegApi
  4. Register in settings/externalRegistry.plugins[id] (Firestore)
  5. Dynamic import the bundle and add the manifest to the in-memory external registry

Errors at any step abort the install with a toast — partial uploads are NOT cleaned up automatically (re-uploading the same plugin overwrites them).

Step 3 — Enable

After install, the plugin appears in the Plugins tab as disabled. Click Enable to activate.

Step 4 — Configure (when applicable)

If the plugin declares a settings page (manifest.settings), an entry appears in the sidebar under Settings at /settings/plugin/<id>. Configure as documented by the plugin author.

Manual install (advanced)

If the upload UI fails for some reason (very large ZIP, corporate firewall blocking the upload, etc.), you can install manually:

  1. Extract the ZIP locally
  2. Upload the extracted folder to /admin/plugins/<id>/ via Flexweg's file manager
  3. In Firestore, create / update settings/externalRegistry:
    {
    "plugins": {
    "<id>": { "version": "1.0.0", "apiVersion": "1.0.0", "installedAt": 1735689600000 }
    }
    }
  4. Reload the admin

The boot loader picks up the new entry and dynamic-imports the bundle.

Common errors

ErrorCauseFix
apiVersion mismatchPlugin built against an API version newer or older than the admin supportsUpdate either the plugin or the admin. Check the plugin's release notes for compatible admin versions
Invalid manifest.idID contains non-ASCII or special charactersPlugin needs a fix from the author
bundle.js not found at <entry>ZIP structure malformedRe-download the plugin's release ZIP
Failed to fetch /admin/plugins/<id>/bundle.jsUpload didn't actually succeedRe-run install, or upload manually via file manager
Cannot find module 'react'Bundle wasn't built with the externals config (React imports got bundled instead of externalised)Plugin needs a fix from the author

How externals integrate with built-ins

The admin's listPlugins() returns [...BUILT_IN_PLUGINS, ...externalPlugins]. Every consumer (PluginsPage, settings routes, publisher hooks) goes through this — so externals appear identically to built-ins. applyPluginRegistration iterates externals after built-ins, so any external filter layers on top (priority still wins, of course).

The boot order guarantees the external set is loaded before applyPluginRegistration runs:

  1. main.tsx sets window.__FLEXWEG_RUNTIME__
  2. <App /> mounts
  3. CmsDataProvider calls loadAllExternalEntries() once
  4. The Firestore subscription gates applyPluginRegistration on externalsLoaded = true

So externals never register late.

Uninstalling

See Uninstalling plugins.

Continue