How to Create Custom Plugins for GeoLibre Using the Plugin API
GeoLibre's Plugin API lets developers extend the mapping application with new controls, panels, layers, and toolbar menus by implementing the GeoLibrePlugin interface and registering the module with the PluginManager, all without touching the core codebase.
The open-source opengeos/GeoLibre project is built around runtime extensibility, and its plugin architecture lets you inject functionality cleanly into both the desktop and web viewers. When you create custom plugins for GeoLibre using the plugin API, you interact with the host application through a typed GeoLibreAppAPI surface and lifecycle hooks.
Define a GeoLibre Plugin
A valid plugin is a TypeScript module that exports an object satisfying the GeoLibrePlugin interface declared in packages/plugins/src/types.ts. At minimum, the object must supply three identifiers and two lifecycle hooks:
id,name, andversion— Unique strings used by thePluginManagerto track the plugin.activate(app: GeoLibreAppAPI)— Called when the plugin is enabled; use this hook to register UI elements, layers, and event listeners via the host API.deactivate(app: GeoLibreAppAPI)— Called when the plugin is disabled; use this hook to tear down every resource created inactivate.
The full type contract and optional hooks—such as URL-parameter handling and state persistence—are documented in docs/plugin-api.md.
The snippet below shows the simplest possible plugin: a custom basemap switcher.
import type { GeoLibreAppAPI, GeoLibrePlugin } from "@geolibre/plugins";
export const myBasemap: GeoLibrePlugin = {
id: "my-basemap",
name: "My Basemap",
version: "0.1.0",
activate(app: GeoLibreAppAPI) {
app.setBasemap("https://example.com/styles/custom.json");
},
deactivate() {
// No cleanup required for a simple basemap switch.
},
};
Register Your Plugin with the PluginManager
Registration paths differ for built-in plugins shipped with GeoLibre and external plugins distributed independently. The PluginManager implementation in packages/plugins/src/plugin-manager.ts handles activation and deactivation for both.
Register a Built-In Plugin
Place the source file under packages/plugins/src/plugins/, export it from packages/plugins/src/index.ts, and wire it into the desktop application via apps/geolibre-desktop/src/hooks/usePlugins.ts. This pattern lets core contributors maintain plugins inside the monorepo while the PluginManager still governs their lifecycle.
Distribute an External Plugin
External plugins are compiled into a zip archive containing a plugin.json manifest. Users install them through Settings > Manage Plugins in the GeoLibre UI, or by pointing to a hosted manifest URL. The external loader delegates to the same PluginManager used by built-in plugins, as described in docs/user-guide/plugins.md.
Use the GeoLibreAppAPI to Extend the Map and UI
Inside activate, the app argument exposes the GeoLibreAppAPI surface defined in packages/plugins/src/types.ts. Because every method is typed as optional with ?, you should invoke capabilities with optional chaining to remain forward-compatible with host variants that may not expose every feature.
Key host capabilities include:
- Basemap control —
app.setBasemap(styleUrl)switches the map style. - Vector layers —
app.addGeoJsonLayer(name, geojson)registers GeoJSON sources that appear in the Layers panel. - Raster layers —
app.addTileLayer(name, url, options?)andapp.addCogLayer(name, url, options?)add XYZ, WMTS, WMS, or COG raster layers. - Zarr support —
app.addZarrLayer(name, url, opts)loads cloud-native Zarr datasets. - Map controls —
app.addMapControl(control, position?)attaches a MapLibreIControl;app.removeMapControl(control)detaches it. - Right-sidebar panels —
app.registerRightPanel({...})creates a dockable panel, whileapp.openRightPanel(id)andapp.closeRightPanel(id)manage visibility. - Toolbar menus —
app.registerToolbarMenu({...})adds a top-level menu with actions and submenus. - Floating panels —
app.registerFloatingPanel({...})creates draggable overlay panels. - Native layer persistence —
app.registerExternalNativeLayer({...})andapp.unregisterExternalNativeLayer(...)integrate directly added MapLibre layers into the Layers panel. - Deck.gl integration —
app.getDeckGL?.()returns the shared Deck.gl context for high-performance 3D rendering. - Cross-plugin control —
app.activatePlugin('other-id')andapp.deactivatePlugin('other-id')toggle other plugins from within your own.
The following example registers a toolbar menu that opens a right-sidebar panel.
import type { GeoLibreAppAPI, GeoLibrePlugin } from "@geolibre/plugins";
export const workbenchPlugin: GeoLibrePlugin = {
id: "workbench",
name: "Workbench",
version: "0.1.0",
activate(app: GeoLibreAppAPI) {
const unregisterPanel = app.registerRightPanel?.({
id: "workbench",
title: "Workbench",
defaultWidth: 360,
render(container) {
const btn = document.createElement("button");
btn.textContent = "Run analysis";
container.appendChild(btn);
return () => btn.remove();
},
});
app.openRightPanel?.("workbench");
const unregisterMenu = app.registerToolbarMenu?.({
id: "workbench-menu",
label: "Workbench",
items: [
{
id: "open",
label: "Open panel",
onSelect: () => app.openRightPanel?.("workbench"),
},
],
});
(this as any)._unregister = () => {
unregisterPanel?.();
unregisterMenu?.();
};
},
deactivate(app: GeoLibreAppAPI) {
app.closeRightPanel?.("workbench");
(this as any)._unregister?.();
},
};
Clean Up Resources in the Deactivate Lifecycle Hook
Every plugin must implement deactivate to prevent memory leaks and orphan UI elements. According to the GeoLibre source code in packages/plugins/src/plugin-manager.ts, you should reverse every mutation made during activate:
- Remove map controls with
app.removeMapControl. - Unregister panels and menus by calling the cleanup functions returned from
registerRightPanel,registerToolbarMenu, andregisterFloatingPanel. - Close persistent panels with
app.closeRightPanel. - Unregister custom layers with
app.unregisterExternalNativeLayer. - Clear timers, event listeners, and any global state your plugin created.
The example below attaches a NavigationControl and then safely removes it.
import type { GeoLibreAppAPI, GeoLibrePlugin } from "@geolibre/plugins";
import { NavigationControl } from "maplibre-gl";
export const navigationPlugin: GeoLibrePlugin = {
id: "navigation-control",
name: "Navigation Control",
version: "0.1.0",
activate(app: GeoLibreAppAPI) {
const control = new NavigationControl();
app.addMapControl(control, "top-left");
(this as any)._control = control;
},
deactivate(app: GeoLibreAppAPI) {
app.removeMapControl?.((this as any)._control);
},
};
Build and Test Your Plugin
GeoLibre provides standard npm scripts for compiling and validating plugins before release.
- Run
npm run buildto produce a production bundle, ornpm run devto start a hot-reloading development server. - Verify type compatibility with the host API by running
npm run test:frontend. - For external plugins, execute
npm run package:geolibreusing the plugin template to generate a zip archive. Install the resulting bundle through the GeoLibre UI to confirm runtime behavior.
Summary
- GeoLibre plugins implement the
GeoLibrePlugininterface defined inpackages/plugins/src/types.ts, requiringid,name,version,activate, anddeactivate. - The
PluginManagerinpackages/plugins/src/plugin-manager.tsorchestrates loading for both built-in plugins and external zip-based bundles. - The
GeoLibreAppAPIexposes methods for basemaps, vector and raster layers, map controls, panels, menus, floating panels, and Deck.gl integration. - Always clean up controls, panels, layers, and listeners inside
deactivateto avoid memory leaks. - Use
npm run build,npm run dev, andnpm run test:frontendduring development; package external plugins withnpm run package:geolibre.
Frequently Asked Questions
What files define the GeoLibre plugin contract and runtime?
The GeoLibrePlugin and GeoLibreAppAPI interfaces are declared in packages/plugins/src/types.ts. The PluginManager class that handles registration, activation, and deactivation lives in packages/plugins/src/plugin-manager.ts. Reference documentation is maintained in docs/plugin-api.md.
Can I build a plugin without modifying the GeoLibre core repository?
Yes. External plugins are compiled independently, packaged as a zip with a plugin.json manifest, and installed at runtime through Settings > Manage Plugins or via a hosted manifest URL. The PluginManager loads them dynamically without requiring changes to GeoLibre's source code.
How do I add a MapLibre custom control to GeoLibre?
Inside activate, instantiate your control and call app.addMapControl(control, position). Save the control reference, then remove it during deactivate by calling app.removeMapControl(control). This pattern is shown in docs/plugin-api.md and aligns with the standard MapLibre IControl interface.
Which API methods are safe to call if the host application does not support every feature?
All GeoLibreAppAPI methods are typed as optional using ?, so you should invoke them with optional chaining, for example app.getDeckGL?.(). This keeps your plugin forward-compatible with host variants that omit specific capabilities.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →