How to Create a Custom Plugin for GeoLibre Using the Plugin Interface
Create a GeoLibre custom plugin by implementing the GeoLibrePlugin interface, registering it with the PluginManager, and using the GeoLibreAppAPI to add map controls, panels, and layers without modifying core code.
The GeoLibre plugin interface, defined in packages/plugins/src/types.ts, provides a standardized contract for extending GeoLibre's mapping capabilities. Plugins run through the PluginManager in packages/plugins/src/plugin-manager.ts, enabling safe activation and deactivation of third-party functionality. This guide walks through the complete process—from interface implementation to cleanup handlers—using patterns directly from the opengeos/GeoLibre source code.
Understanding the GeoLibrePlugin Interface
Every custom plugin must satisfy the GeoLibrePlugin type definition. The interface requires three identification properties and two lifecycle methods.
Required Plugin Properties
id– Unique string identifier (kebab-case recommended)name– Human-readable display nameversion– Semantic version string
Required Lifecycle Methods
activate(app: GeoLibreAppAPI)– Called when the plugin is enabled; use this to register UI elements, layers, and event listenersdeactivate(app: GeoLibreAppAPI)– Called when the plugin is disabled; must remove all resources added during activation
The GeoLibreAppAPI type, documented in docs/plugin-api.md, exposes methods for interacting with the map, sidebar panels, toolbars, and layer management.
Step-by-Step: Building Your First Plugin
Step 1: Create the Plugin Module
Start with a TypeScript file that imports the core types and exports a GeoLibrePlugin object. The simplest valid plugin switches the basemap when activated:
import type { GeoLibreAppAPI, GeoLibrePlugin } from "@geolibre/plugins";
export const myBasemapPlugin: GeoLibrePlugin = {
id: "my-custom-basemap",
name: "My Custom Basemap",
version: "1.0.0",
activate(app: GeoLibreAppAPI) {
app.setBasemap("https://example.com/styles/terrain.json");
},
deactivate() {
// Basemap persists until user changes it; no cleanup required
},
};
This pattern matches the sample code in docs/plugin-api.md lines 41-48. The setBasemap method immediately applies a new MapLibre style URL to the active map.
Step 2: Register UI Components with the Host API
Most plugins add interactive elements. The GeoLibreAppAPI provides methods across several categories:
Map controls – addMapControl(control, position?) and removeMapControl(control)
Sidebar panels – registerRightPanel(config), openRightPanel(id), closeRightPanel(id)
Toolbar menus – registerToolbarMenu(config)
Floating panels – registerFloatingPanel(config)
Layer registration – addGeoJsonLayer, addTileLayer, addCogLayer, addZarrLayer
External native layers – registerExternalNativeLayer for direct MapLibre layer integration
Deck.gl access – getDeckGL() for 3D visualization
All API methods are optional (typed with ?) to maintain forward compatibility with host variants that may not expose every feature.
Step 3: Handle Cleanup in Deactivate
The deactivate method prevents memory leaks and orphaned UI elements. Store references to registered components during activation, then remove them during deactivation:
activate(app: GeoLibreAppAPI) {
// Register components and store cleanup functions
const unregisterPanel = app.registerRightPanel?.({...});
const unregisterMenu = app.registerToolbarMenu?.({...});
// Store for later cleanup
this._cleanup = () => {
unregisterPanel?.();
unregisterMenu?.();
};
},
deactivate(app: GeoLibreAppAPI) {
// Execute all cleanup functions
this._cleanup?.();
app.closeRightPanel?.("my-panel-id");
}
The PluginManager in packages/plugins/src/plugin-manager.ts guarantees deactivate is called before a plugin is removed, even during application shutdown or plugin replacement.
Complete Examples: Common Plugin Patterns
Example: Toolbar Menu with Right Sidebar Panel
This pattern combines a top-level menu item with a dockable sidebar panel, following the implementation style in docs/plugin-api.md lines 443-466:
import type { GeoLibreAppAPI, GeoLibrePlugin } from "@geolibre/plugins";
export const analysisWorkbenchPlugin: GeoLibrePlugin = {
id: "analysis-workbench",
name: "Analysis Workbench",
version: "2.1.0",
activate(app: GeoLibreAppAPI) {
// Register right sidebar panel
const unregisterPanel = app.registerRightPanel?.({
id: "workbench-panel",
title: "Analysis Workbench",
defaultWidth: 400,
icon: "🔬",
render(container) {
const header = document.createElement("h3");
header.textContent = "Available Tools";
const list = document.createElement("ul");
["Buffer", "Intersect", "Union"].forEach(tool => {
const li = document.createElement("li");
li.textContent = tool;
list.appendChild(li);
});
container.appendChild(header);
container.appendChild(list);
// Return cleanup function for this render
return () => {
container.innerHTML = "";
};
},
});
// Register toolbar menu with action
const unregisterMenu = app.registerToolbarMenu?.({
id: "workbench-menu",
label: "Analysis",
icon: "🔬",
items: [
{
id: "open-workbench",
label: "Open Workbench",
onSelect: () => app.openRightPanel?.("workbench-panel"),
},
{
id: "run-quick-buffer",
label: "Quick Buffer (100m)",
onSelect: () => {
// Execute analysis directly
console.log("Running 100m buffer...");
},
},
],
});
// Open panel immediately on plugin activation
app.openRightPanel?.("workbench-panel");
// Store cleanup functions
(this as any)._dispose = () => {
unregisterPanel?.();
unregisterMenu?.();
};
},
deactivate(app: GeoLibreAppAPI) {
app.closeRightPanel?.("workbench-panel");
(this as any)._dispose?.();
},
};
Example: Custom MapLibre Control
Add native MapLibre controls using the optional control API from docs/plugin-api.md lines 109-113:
import type { GeoLibreAppAPI, GeoLibrePlugin } from "@geolibre/plugins";
import { ScaleControl, FullscreenControl } from "maplibre-gl";
export const enhancedControlsPlugin: GeoLibrePlugin = {
id: "enhanced-controls",
name: "Enhanced Map Controls",
version: "1.0.0",
_controls: [] as maplibregl.IControl[],
activate(app: GeoLibreAppAPI) {
const scale = new ScaleControl({ maxWidth: 200, unit: "metric" });
const fullscreen = new FullscreenControl();
app.addMapControl(scale, "bottom-left");
app.addMapControl(fullscreen, "top-right");
this._controls = [scale, fullscreen];
},
deactivate(app: GeoLibreAppAPI) {
this._controls.forEach(control => {
app.removeMapControl?.(control);
});
this._controls = [];
},
};
Example: Layer Registration with Persistence
Register custom layers that appear in GeoLibre's Layers panel and participate in state persistence:
import type { GeoLibreAppAPI, GeoLibrePlugin } from "@geolibre/plugins";
export const seismicDataPlugin: GeoLibrePlugin = {
id: "seismic-data",
name: "Real-time Seismic Data",
version: "1.2.0",
_layerId: null as string | null,
_pollInterval: null as number | null,
async activate(app: GeoLibreAppAPI) {
// Fetch and display GeoJSON layer
const response = await fetch("https://api.example.com/earthquakes.geojson");
const geojson = await response.json();
this._layerId = app.addGeoJsonLayer("Recent Earthquakes", geojson, {
cluster: true,
clusterMaxZoom: 14,
paint: {
"circle-color": "#ff4500",
"circle-radius": ["step", ["get", "mag"], 4, 5, 8, 7, 12],
},
});
// Set up polling for updates
this._pollInterval = window.setInterval(async () => {
const fresh = await fetch("https://api.example.com/earthquakes.geojson");
// Layer update logic would go here
}, 60000);
},
deactivate(app: GeoLibreAppAPI) {
if (this._pollInterval) {
clearInterval(this._pollInterval);
}
// Layer is automatically removed when plugin deactivates
// For manual cleanup: app.removeLayer?.(this._layerId)
},
};
Registering Your Plugin: Built-in vs. External
Built-in Plugin Registration
For plugins shipped with GeoLibre, follow this file structure:
- Create source file in
packages/plugins/src/plugins/your-plugin.ts - Export from
packages/plugins/src/index.ts - Register in
apps/geolibre-desktop/src/hooks/usePlugins.ts
The usePlugins.ts hook initializes the PluginManager and loads built-in plugins at application startup. Example registration pattern:
import { myBasemapPlugin } from "@geolibre/plugins";
// Inside usePlugins hook
pluginManager.registerBuiltIn(myBasemapPlugin);
External Plugin Distribution
For independently distributed plugins:
- Compile to JavaScript bundle
- Create
plugin.jsonmanifest with metadata matching yourGeoLibrePluginidentifiers - Package as zip archive
- Install via Settings > Manage Plugins in GeoLibre desktop, or host manifest URL for network installation
The PluginManager loads external plugins through the same activation/deactivation flow, sandboxing them from core application code.
Build, Test, and Package
GeoLibre provides tooling for each development phase:
- Development:
npm run devstarts hot-reloading dev server - Type checking:
npm run test:frontendvalidates TypeScript types against host API - Production build:
npm run buildcompiles optimized plugin bundle - Packaging:
npm run package:geolibre(from plugin template) generates installable zip with manifest
Run integration tests by installing the packaged plugin through the GeoLibre UI and verifying activation, functionality, and clean deactivation.
Key Source Files for Plugin Development
| File | Purpose |
|---|---|
packages/plugins/src/types.ts |
GeoLibrePlugin and GeoLibreAppAPI type definitions |
packages/plugins/src/plugin-manager.ts |
Core runtime for plugin lifecycle management |
apps/geolibre-desktop/src/hooks/usePlugins.ts |
Built-in plugin registration boilerplate |
docs/plugin-api.md |
Complete API reference with all optional methods |
docs/user-guide/plugins.md |
End-user documentation for plugin installation |
Summary
- Implement the interface: Create a TypeScript module exporting a
GeoLibrePluginobject withid,name,version,activate, anddeactivate - Use the host API: Call methods on
GeoLibreAppAPIto add map controls (addMapControl), sidebar panels (registerRightPanel), toolbar menus (registerToolbarMenu), and layers (addGeoJsonLayer,addTileLayer, etc.) - Clean up completely: Remove all registered components in
deactivateto prevent memory leaks and orphaned UI elements - Register appropriately: Add built-in plugins through
usePlugins.tsor distribute external plugins as zip packages withplugin.jsonmanifests - Reference the source: Type definitions in
packages/plugins/src/types.tsand thePluginManagerimplementation inpackages/plugins/src/plugin-manager.tsdefine the exact contract your plugin must satisfy
Frequently Asked Questions
What is the minimum code required for a valid GeoLibre plugin?
A valid plugin requires five properties: id, name, version, activate(app), and deactivate(app). The simplest implementation provides empty lifecycle methods, though practical plugins use activate to register functionality with the GeoLibreAppAPI. See the basemap example in docs/plugin-api.md lines 41-48 for a minimal working template.
Can a plugin control other plugins?
Yes. The GeoLibreAppAPI includes optional lifecycle helpers activatePlugin(otherId) and deactivatePlugin(otherId). These methods enable plugin orchestration—for example, a "Survey Mode" plugin that automatically activates measurement tools and layer visibility controls. All API methods are optional (?) typed to ensure forward compatibility.
How do I persist plugin state between sessions?
GeoLibre handles persistence for registered layers, panels, and map state automatically. For custom plugin-specific data, use URL parameters (handled via optional plugin hooks) or the browser's localStorage/IndexedDB. The registerExternalNativeLayer method ensures direct MapLibre layers appear in the Layers panel and participate in built-in state serialization.
What happens if my plugin throws an error during activation?
The PluginManager in packages/plugins/src/plugin-manager.ts wraps plugin activation in error handling. A failed activation marks the plugin as errored and prevents it from running, but does not crash the host application. Check the browser console and GeoLibre's internal logs for stack traces. Defensive coding in activate—checking for optional API methods with ?.—prevents common failures.
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 →