How to Create a Custom Plugin for GeoLibre Using the Plugin API
To create a custom plugin for GeoLibre, implement the GeoLibrePlugin interface with required metadata fields and lifecycle methods (activate and deactivate), then register your plugin through the PluginManager located in packages/plugins/src/plugin-manager.ts.
GeoLibre is an open-source geospatial visualization platform built on MapLibre GL JS. Its Plugin API enables developers to extend core functionality—adding map controls, custom panels, toolbar menus, and data layers—without modifying the underlying source code. This guide walks through the complete process of building, registering, and deploying plugins using the official type definitions and host API contracts.
Understanding the GeoLibre Plugin Architecture
The plugin system centers on two core components: the interface definition and the runtime manager.
In packages/plugins/src/types.ts, the GeoLibrePlugin interface defines the contract every plugin must satisfy. At minimum, your plugin object must include:
id: A unique string identifiername: Human-readable display nameversion: Semantic version stringactivate(app): Lifecycle hook receivingGeoLibreAppAPIto interact with the hostdeactivate(app): Cleanup hook to remove UI elements and listeners
The PluginManager class in packages/plugins/src/plugin-manager.ts handles the runtime lifecycle, invoking activate() when users enable your plugin and deactivate() when disabled. For built-in plugins, registration occurs in apps/geolibre-desktop/src/hooks/usePlugins.ts, while external plugins are loaded dynamically via manifest URLs.
Step-by-Step: Creating Your First Custom Plugin
1. Implement the GeoLibrePlugin Interface
Create a TypeScript module that exports a GeoLibrePlugin object. The activate() method receives the GeoLibreAppAPI (full contract documented in docs/plugin-api.md), which provides methods to manipulate the map, UI, and data layers.
import type { GeoLibreAppAPI, GeoLibrePlugin } from "@geolibre/plugins";
export const myPlugin: GeoLibrePlugin = {
id: "my-custom-plugin",
name: "My Custom Plugin",
version: "1.0.0",
activate(app: GeoLibreAppAPI) {
// Plugin initialization logic here
},
deactivate(app: GeoLibreAppAPI) {
// Cleanup logic here
}
};
2. Register Your Plugin
For built-in plugins: Place your source file under packages/plugins/src/plugins/, export it from packages/plugins/src/index.ts, and import it into apps/geolibre-desktop/src/hooks/usePlugins.ts to register with the PluginManager.
For external plugins: Package your compiled bundle as a zip file containing a plugin.json manifest. Users install these via Settings > Manage Plugins, or you can host the manifest URL for direct loading. The external loader uses the same PluginManager to load and activate at runtime.
3. Interact with the Host Application
Inside activate(app), leverage the GeoLibreAppAPI to extend functionality:
- Basemap control:
app.setBasemap(styleUrl)switches map styles - Vector layers:
app.addGeoJsonLayer(name, geojson)registers GeoJSON sources - Raster layers:
app.addTileLayer(name, url, options)for XYZ tiles, orapp.addCogLayer(name, url, options)for Cloud Optimized GeoTIFFs - Zarr support:
app.addZarrLayer(name, url, opts)loads cloud-native Zarr datasets - Map controls:
app.addMapControl(control, position)attaches MapLibreIControlinstances - UI panels:
app.registerRightPanel({...})creates dockable sidebar panels,app.registerFloatingPanel({...})creates draggable overlays - Toolbar menus:
app.registerToolbarMenu({...})adds top-level application menus - Deck.gl integration:
app.getDeckGL?.()accesses shared Deck.gl modules for 3D rendering - Plugin orchestration:
app.activatePlugin('id')andapp.deactivatePlugin('id')control other plugins
All API methods are optional (typed with ?) to ensure forward compatibility across host variants.
4. Handle Cleanup in Deactivate
The deactivate() method must reverse all changes made during activation to prevent memory leaks and orphaned UI elements:
- Remove map controls using
app.removeMapControl(control) - Close and unregister panels via
app.closeRightPanel(id)and the cleanup functions returned by registration methods - Unregister custom layers with
app.unregisterExternalNativeLayer(id) - Clear timers, event listeners, and global state
Complete Code Examples for GeoLibre Plugins
Example 1: Simple Basemap Switcher
This minimal plugin switches to a custom map style when activated:
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() {
// Basemap persists until user changes it; no cleanup required
},
};
Example 2: Toolbar Menu with Right Sidebar Panel
This plugin creates a workbench panel accessible via both the right sidebar and a toolbar menu:
import type { GeoLibreAppAPI, GeoLibrePlugin } from "@geolibre/plugins";
export const workbenchPlugin: GeoLibrePlugin = {
id: "workbench",
name: "Workbench",
version: "0.1.0",
activate(app: GeoLibreAppAPI) {
// Register right sidebar panel
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();
},
});
// Open panel immediately
app.openRightPanel?.("workbench");
// Register toolbar menu
const unregisterMenu = app.registerToolbarMenu?.({
id: "workbench-menu",
label: "Workbench",
items: [
{
id: "open",
label: "Open panel",
onSelect: () => app.openRightPanel?.("workbench")
},
],
});
// Store cleanup functions
(this as any)._cleanup = () => {
unregisterPanel?.();
unregisterMenu?.();
};
},
deactivate(app: GeoLibreAppAPI) {
app.closeRightPanel?.("workbench");
(this as any)._cleanup?.();
},
};
Example 3: Custom MapLibre Control
Demonstrates adding a native MapLibre control and removing it on deactivation:
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);
},
};
Building and Testing Your Plugin
Compile your plugin using the standard npm scripts:
npm run build: Production buildnpm run dev: Hot-reloading development servernpm run test:frontend: Run TypeScript type checking against the host APInpm run package:geolibre: Generate distributable zip for external plugins (available via plugin template)
For external plugins, install the generated zip through the GeoLibre UI to verify runtime behavior.
Summary
- Implement the
GeoLibrePlugininterface defined inpackages/plugins/src/types.tswith mandatoryid,name,version,activate(), anddeactivate()properties - Access the host application capabilities through the
GeoLibreAppAPIobject passed to lifecycle methods - Register built-in plugins in
apps/geolibre-desktop/src/hooks/usePlugins.tsor distribute external plugins as zip files withplugin.jsonmanifests - Clean up all UI elements, map controls, and event listeners in the
deactivate()method to prevent memory leaks - Use the
PluginManagerinpackages/plugins/src/plugin-manager.tsto handle plugin lifecycle and state management
Frequently Asked Questions
What is the minimum code required to create a valid GeoLibre plugin?
At minimum, you must export an object satisfying the GeoLibrePlugin interface with a unique id, name, version, and the activate(app) method. The deactivate() method is strongly recommended for cleanup but technically optional depending on your plugin's side effects.
How do I add a custom button to the GeoLibre toolbar?
Use the app.registerToolbarMenu() method inside your activate() function. Pass a configuration object with an id, label, and items array containing your button actions. Store the returned cleanup function and call it during deactivate() to remove the menu.
Can my plugin interact with other installed plugins?
Yes. The GeoLibreAppAPI provides activatePlugin('plugin-id') and deactivatePlugin('plugin-id') methods that allow your plugin to programmatically control other plugins. You can also check plugin states through the PluginManager if you have access to the app instance.
Where should I place my plugin code for development versus production?
During development, place built-in plugins under packages/plugins/src/plugins/ for integration with the monorepo's hot-reloading. For production distribution as an external plugin, compile to a standalone bundle, package it with a plugin.json manifest using npm run package:geolibre, and install via the GeoLibre Settings UI.
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 →