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 buildproduces a ZIP underdist/) - 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: Plugins → Install 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
.zipor click to select
After picking the file:
- Client-side ZIP extraction via JSZip. macOS-style wrapping folders (
__MACOSX/, single top-level dir) are auto-flattened. - Validation:
manifest.jsonexists and parsesmanifest.idis sanitised (lower-case ASCII + dashes)manifest.apiVersionfalls within the admin's[FLEXWEG_API_MIN_VERSION, FLEXWEG_API_VERSION]rangebundle.jsexists at the path declared inmanifest.entry
- Upload every file under
/admin/plugins/<id>/via flexwegApi - Register in
settings/externalRegistry.plugins[id](Firestore) - 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:
- Extract the ZIP locally
- Upload the extracted folder to
/admin/plugins/<id>/via Flexweg's file manager - In Firestore, create / update
settings/externalRegistry:{"plugins": {"<id>": { "version": "1.0.0", "apiVersion": "1.0.0", "installedAt": 1735689600000 }}} - Reload the admin
The boot loader picks up the new entry and dynamic-imports the bundle.
Common errors
| Error | Cause | Fix |
|---|---|---|
apiVersion mismatch | Plugin built against an API version newer or older than the admin supports | Update either the plugin or the admin. Check the plugin's release notes for compatible admin versions |
Invalid manifest.id | ID contains non-ASCII or special characters | Plugin needs a fix from the author |
bundle.js not found at <entry> | ZIP structure malformed | Re-download the plugin's release ZIP |
Failed to fetch /admin/plugins/<id>/bundle.js | Upload didn't actually succeed | Re-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:
main.tsxsetswindow.__FLEXWEG_RUNTIME__<App />mountsCmsDataProvidercallsloadAllExternalEntries()once- The Firestore subscription gates
applyPluginRegistrationonexternalsLoaded = true
So externals never register late.
Uninstalling
See Uninstalling plugins.