How to Implement the Plugin API for Third-Party GeoLibre Extensions
GeoLibre exposes a typed, store-driven plugin system through the @geolibre/plugins package that lets developers add map controls, layers, and UI panels by implementing the GeoLibrePlugin interface and registering lifecycle hooks with the PluginManager.
The GeoLibre plugin API provides a contract-based architecture for extending the core mapping application without modifying source code. Whether you are building custom map controls, specialized data visualizations, or integrated sidebar panels, the system defined in docs/plugin-api.md offers strongly-typed hooks for seamless integration. This guide walks through implementing third-party extensions using the official interfaces and registration patterns found in the opengeos/GeoLibre repository.
Understanding the Plugin Contract
The foundation of the GeoLibre plugin API rests on two core TypeScript interfaces that define the boundary between the host application and third-party code.
The GeoLibrePlugin Interface
Every extension must export a plain JavaScript object that conforms to the GeoLibrePlugin interface defined in docs/plugin-api.md【L26-L46】. This contract requires:
id: A unique string identifier for the pluginname: A human-readable display nameversion: Semantic versioning stringactivate(app): A lifecycle hook receiving theGeoLibreAppAPIinstance where you initialize controls and UIdeactivate(app): A cleanup hook to remove listeners and UI componentshandleUrlParameters: (Optional) Called when URL parameters declared inurlParameterNamesare detected after the map initializes【L70-L88】
The GeoLibreAppAPI Surface
The host passes a GeoLibreAppAPI instance to your lifecycle hooks, providing typed methods for map manipulation and UI registration【L58-L155】. Key capabilities include:
- Layer management:
addGeoJsonLayer,addTileLayer,addCogLayer,addZarrLayer, andregisterExternalNativeLayerfor custom WebGL layers - UI registration:
registerRightPanel,registerFloatingPanel, andregisterToolbarMenu - Map controls:
addMapControlandremoveMapControlfor positioning custom MapLibre controls - State management: Direct access to the GeoLibre store ensures layers survive project saves and appear in the built-in Layers panel
The Plugin Lifecycle
GeoLibre manages extensions through a strict three-phase lifecycle implemented in packages/plugins/src/plugin-manager.ts.
1. Registration
Built-in plugins are registered by exporting them from packages/plugins/src/plugins/ and re-exporting from packages/plugins/src/index.ts. The PluginManager maintains an internal registry of available plugins.
2. Activation
When a user enables the plugin (or when the app starts for auto-loading plugins), the manager calls activate(app). This is where you:
- Call registration helpers like
app.registerRightPanelorapp.addMapControl - Set up event listeners
- Store cleanup functions for later use
3. Deactivation
When the plugin is disabled or the app shuts down, the manager calls deactivate(app). You must:
- Call stored unregister functions (e.g.,
app.removeMapControl) - Remove DOM elements you created
- Unsubscribe from events to prevent memory leaks
Extending Map Functionality
Third-party extensions can modify the map view through two primary pathways: adding controls and registering layers.
Adding Map Controls
To add a custom MapLibre control, instantiate your control in activate and position it using the MapLibre position syntax ("top-right", "bottom-left", etc.). Always store the control reference for removal during deactivation.
import type { GeoLibreAppAPI, GeoLibrePlugin } from "@geolibre/plugins";
import { ZoomControl } from "maplibre-gl-zoom-control";
export const zoomPlugin: GeoLibrePlugin = {
id: "zoom-plugin",
name: "Zoom Control",
version: "0.1.0",
activate(app: GeoLibreAppAPI) {
const ctrl = new ZoomControl();
app.addMapControl(ctrl, "top-right");
(this as any)._ctrl = ctrl;
},
deactivate(app) {
app.removeMapControl((this as any)._ctrl);
},
};
Registering Layers
Use the typed layer helpers to ensure proper store integration. When you call addGeoJsonLayer or addTileLayer, GeoLibre automatically registers the layer in the application state, making it visible in the Layers panel and persistent across project saves【L58-L84】.
For custom WebGL layers that bypass GeoLibre's abstractions, you must call registerExternalNativeLayer after adding the native MapLibre layer. This allows the layer to participate in the paint editing UI and appear in layer listings【L444-L466】.
Building UI Components
The GeoLibre plugin API supports three UI extension points: right-sidebar panels, floating panels, and toolbar menus.
Right-Sidebar Panels
Register dockable panels that share space with the built-in Style and Layers sidebars using app.registerRightPanel. The method accepts a configuration object with an id, title, defaultWidth, and a render callback.
The render function receives a plain DOM HTMLElement as its container. You must return a cleanup function that removes any elements you added, as GeoLibre calls this during panel closure or plugin deactivation【L98-L130】.
activate(app: GeoLibreAppAPI) {
const unregister = app.registerRightPanel?.({
id: "analysis-panel",
title: "Spatial Analysis",
defaultWidth: 360,
render(container) {
const div = document.createElement("div");
div.textContent = "Analysis tools";
container.appendChild(div);
return () => div.remove(); // Cleanup required
},
});
(this as any)._unregisterPanel = unregister;
app.openRightPanel?.("analysis-panel");
}
Floating Panels
For draggable overlays that do not shrink the map viewport, use app.registerFloatingPanel. These panels float above the map canvas and are ideal for tools that require constant visibility while panning or zooming【L146-L170】.
Toolbar Menus
Add top-level application menus via app.registerToolbarMenu. Items support actions, nested submenus, and separators. External plugin menus are automatically appended after the built-in Help menu【L44-L71】.
activate(app) {
const unregister = app.registerToolbarMenu?.({
id: "custom-tools",
label: "Tools",
items: [
{ id: "action-1", label: "Process Data", onSelect: () => processData() },
{ type: "separator" },
{ id: "submenu", label: "Advanced", items: [...] }
],
});
(this as any)._unregisterMenu = unregister;
}
Packaging and Distribution
GeoLibre supports two distribution models for third-party extensions.
Built-in Plugins
Compiled into the main application bundle, built-in plugins reside in packages/plugins/src/plugins/ and are registered in apps/geolibre-desktop/src/hooks/usePlugins.ts. This approach requires fork access and follows the three-step registration process documented in docs/plugin-api.md【L39-L80】.
External Plugins
Distributed as ZIP bundles containing a plugin.json manifest, an entry JavaScript file, and optional CSS. Users install these through the Manage Plugins UI or via manifest URLs. The official plugin template at opengeos/geolibre-plugin-template provides the required webpack configuration and type definitions for external development.
Complete Implementation Example
The following minimal plugin demonstrates URL-parameter-driven loading, right-panel registration, and proper cleanup:
import type { GeoLibreAppAPI, GeoLibrePlugin } from "@geolibre/plugins";
export const myPlugin: GeoLibrePlugin = {
id: "my-plugin",
name: "My Plugin",
version: "0.1.0",
urlParameterNames: ["myData"],
activate(app: GeoLibreAppAPI) {
// Add a custom basemap
app.setBasemap("https://example.com/style.json");
// Register sidebar panel with cleanup
const unregister = app.registerRightPanel?.({
id: "my-panel",
title: "My Panel",
defaultWidth: 360,
render(container) {
const btn = document.createElement("button");
btn.textContent = "Run analysis";
container.appendChild(btn);
return () => btn.remove();
},
});
(this as any)._unregister = unregister;
app.openRightPanel?.("my-panel");
},
deactivate() {
(this as any)._unregister?.();
},
async handleUrlParameters(app, params) {
for (const url of params.getAll("myData")) {
try {
const resp = await fetch(url);
if (!resp.ok) continue;
const geojson = await resp.json();
app.addGeoJsonLayer("URL layer", geojson, url);
} catch {
// Silently skip malformed URLs
}
}
},
};
Summary
- Implement
GeoLibrePlugin: Export an object withid,name,version,activate, anddeactivatehooks as defined indocs/plugin-api.md【L26-L46】. - Use
GeoLibreAppAPI: Access the host surface through theappparameter to register layers, controls, and UI panels with full TypeScript support【L58-L155】. - Manage Lifecycle: Store cleanup functions during
activateand invoke them duringdeactivateto prevent memory leaks and UI artifacts. - Choose Distribution: Package as built-in plugins in
packages/plugins/src/plugins/for core contributions, or as external ZIP bundles for end-user installation. - Handle URLs: Implement
handleUrlParametersand declareurlParameterNamesto enable deep-linking and automated data loading.
Frequently Asked Questions
How do I access the map instance directly from a plugin?
The GeoLibreAppAPI intentionally abstracts the raw MapLibre GL JS instance to ensure state consistency. However, when adding native layers via registerExternalNativeLayer, you interact with the map's style specification directly. For advanced use cases requiring the map object, inspect the internal implementation in packages/plugins/src/types.ts, though this may break during minor version updates.
Can plugins communicate with each other?
GeoLibre does not provide a direct inter-plugin messaging bus. Plugins should remain isolated and communicate through the shared GeoLibre store state or by emitting custom events on the DOM window object. Built-in plugins in packages/plugins/src/plugins/ demonstrate this pattern by reacting to the same store slices rather than direct method invocation.
What is the difference between addGeoJsonLayer and registerExternalNativeLayer?
addGeoJsonLayer creates a fully managed GeoLibre layer that appears in the Layers panel, supports styling through the Style editor, and persists in project saves. registerExternalNativeLayer is for custom WebGL layers added directly to MapLibre's style; it informs GeoLibre's UI that the layer exists so it can participate in paint property editing, but you retain full control over the source and rendering logic【L444-L466】.
How do I debug a plugin that fails to activate?
Check the browser console for TypeScript errors indicating missing required properties on your GeoLibrePlugin object. Verify that activate is not throwing synchronous errors, as these prevent the plugin from entering the active state. For external plugins, validate that your plugin.json manifest specifies the correct entry file path and that all dependencies are bundled or available as globals.
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 →