GeoLibre Plugin API: How to Create Custom Plugins for Maps and Geospatial Data
GeoLibre's plugin API exposes a typed JavaScript interface that lets developers create custom plugins with full control over maps, layers, UI panels, toolbar menus, and cross-plugin coordination.
The GeoLibrePlugin interface forms the core contract between the host application and your code. Whether you're building for desktop, web, or Jupyter environments, the same API surface gives you programmatic access to MapLibre GL, Deck.gl, cloud-optimized data formats, and the complete geospatial stack. This guide breaks down every available method with source-backed examples from the opengeos/GeoLibre repository.
GeoLibrePlugin Interface: The Foundation
Every plugin must implement the GeoLibrePlugin interface defined in docs/plugin-api.md. This contract specifies your plugin's identity, lifecycle, and optional capabilities.
Required Properties
id: Unique string identifier (kebab-case recommended)name: Human-readable display nameversion: SemVer string for compatibility checks
Optional Properties
activeByDefault: Boolean for auto-activation on startupurlParameterNames: Array of URL parameters this plugin handles
Lifecycle Methods
interface GeoLibrePlugin {
activate(app: GeoLibreAppAPI): void | Promise<void>;
deactivate(app: GeoLibreAppAPI): void | Promise<void>;
handleUrlParameters?(app: GeoLibreAppAPI, params: Record<string, string>): void;
}
The host calls activate() when a user enables your plugin, passing a fully populated GeoLibreAppAPI instance. The deactivate() hook triggers on disable or unload—use it to clean up layers, panels, and event listeners.
GeoLibreAppAPI: Complete Method Reference
The GeoLibreAppAPI object exposes 30+ methods across eight functional areas. All methods are optional (typed as possibly undefined), so defensive coding with optional chaining is recommended.
Map and Basemap Control
| Method | Purpose |
|---|---|
setBasemap(styleUrl) |
Switch base style via MapLibre GL style URL |
getActiveBasemap() |
Retrieve current base style URL |
addMapControl(control, position?) |
Inject custom IControl at specified corner |
removeMapControl(control) |
Remove previously added control |
setBuiltInMapControlVisible(control, visible) |
Toggle native controls (navigation, fullscreen, terrain) |
setBuiltInMapControlPosition(control, position) |
Relocate built-in controls |
Position values: top-left, top-right, bottom-left, bottom-right from the GeoLibreMapControlPosition enum.
Layer Management: Vector, Raster, and Cloud-Native
Vector data
addGeoJsonLayer(name, data, sourcePath?)— Registers GeoJSON as a native layer with automatic UI panel integration and project persistence
Raster and tile layers
addTileLayer(name, url, options?)— XYZ tile sourcesaddWmtsLayer(name, url, options?)— WMTS protocoladdWmsLayer(name, options)— WMS with layer/style configuration
Cloud-Optimized GeoTIFF (COG)
const layerId = await app.addCogLayer?.(
"Elevation",
"https://example.com/dem.tif",
{
colormap: "terrain",
opacity: 0.9,
nodata: -9999
}
);
Returns Promise<string> resolving to the layer ID. Supports band selection, custom colormaps, and nodata handling per docs/plugin-api.md#app-addcoglayer.
Zarr data cubes
addZarrLayer(name, url, options)— ReturnsPromise<string>; renders v2/v3 Zarr storessetZarrLayerSelector(layerId, selector)— Update non-spatial dimensions (time, altitude)queryZarrLayer(layerId, geometry, selector?, options?)— Point/polygon value extraction for identify operations
Custom WebGL Layer Integration
registerExternalNativeLayer(registration) and unregisterExternalNativeLayer(id) bridge layers added directly through MapLibre's CustomLayerInterface into GeoLibre's state management. This enables:
- Layer appearance in the built-in Layers panel
- Project file persistence
- Optional
paintBridgefor opacity/visibility synchronization
UI Surface Registration
Right-sidebar panels (dockable, resizable)
const unregister = app.registerRightPanel?.({
id: "my-panel",
title: "Analysis Results",
defaultWidth: 350,
render(container: HTMLElement) {
// Build DOM directly—no React dependency required
const root = document.createElement("div");
root.textContent = "Ready";
container.appendChild(root);
// Return cleanup function
return () => root.remove();
}
});
app.openRightPanel?.("my-panel");
Control methods: openRightPanel(id), closeRightPanel(id), collapseRightPanel(id).
Floating panels (draggable overlays)
registerFloatingPanel(panel)/unregisterFloatingPanel(id)openFloatingPanel(id),closeFloatingPanel(id),getOpenFloatingPanels()
Toolbar menus
registerToolbarMenu(menu) adds top-level menus to the GeoLibre banner with full submenu support. Menus support actions, separators, and nested hierarchies.
Temporal and Animation Support
registerTemporalLayer(layerId, adapter, { bind? }) links any layer to the global Time Slider. The adapter translates temporal state (current frame, playback direction) into layer-appropriate updates. Essential for animated rasters, time-series visualization, or custom frame-based layers.
Cross-Plugin Coordination
Plugins can programmatically manage other plugins:
// Activate with optional state injection
await app.activatePlugin?.("chart-plugin", {
dataSource: "temperature-series",
style: "line"
});
// Graceful shutdown
await app.deactivatePlugin?.("chart-plugin");
Use getProjectState() and applyProjectState(state) to serialize plugin settings into .geolibre project files and restore them on load.
Deck.gl Integration
getDeckGL() returns a promise resolving to the host's shared Deck.gl modules:
const { core, layers, geoLayers, meshLayers, mapbox } = await app.getDeckGL?.();
Rendering on this single instance prevents version conflicts and ensures proper z-ordering with MapLibre layers.
Utility Methods
| Method | Use case |
|---|---|
fetchArrayBuffer(url) |
Binary asset loading (COG headers, etc.) |
fitBounds(bounds) |
Zoom to [west, south, east, north] extent |
getMap() |
Access underlying MapLibre Map instance |
Plugin Architecture and Runtime Flow
Understanding how the host loads and manages plugins ensures predictable behavior across environments.
Registration and Discovery
The PluginManager in packages/plugins/src/plugin-manager.ts maintains a registry of PluginRegistryEntry objects (apps/geolibre-desktop/src/lib/plugin-registry.ts). At startup:
- Built-in plugins load from
packages/plugins/src/index.ts - Bundled drop-ins load from
public/plugins/ - External plugins discovered in user directories load via
usePlugins.ts
Activation Lifecycle
Host initializes → PluginManager.register(plugin) → User enables plugin
↓
plugin.activate(app) with GeoLibreAppAPI
↓
Plugin calls API methods, registers UI
↓
User disables plugin → plugin.deactivate(app)
State changes propagate automatically to the Zustand-based global store, synchronizing the Layers panel, style editor, and time slider.
External Plugin Packaging
External plugins distribute as:
- Zip archives:
plugin.jsonmanifest + compileddist/*.js - Directory bundles: Same structure, loaded directly from filesystem
Integrity verification runs through plugin-integrity.ts (hashing, manifest validation) with trust decisions handled by plugin-trust.ts.
Complete Plugin Examples
Example 1: Basemap Switcher
import type { GeoLibreAppAPI, GeoLibrePlugin } from "@geolibre/plugins";
export const basemapSwitcher: GeoLibrePlugin = {
id: "basemap-switcher",
name: "Basemap Switcher",
version: "0.1.0",
activate(app: GeoLibreAppAPI) {
app.setBasemap?.(
"https://basemaps.cartocdn.com/gl/positron-gl-style/style.json"
);
},
deactivate() {
// Intentionally minimal—preserve user's chosen style
}
};
From docs/plugin-api.md#geolibreplugin.
Example 2: COG Viewer with Map Control
import type { GeoLibreAppAPI, GeoLibrePlugin } from "@geolibre/plugins";
let navControl: maplibregl.NavigationControl | null = null;
export const cogViewer: GeoLibrePlugin = {
id: "cog-viewer",
name: "COG Viewer",
version: "0.2.0",
async activate(app: GeoLibreAppAPI) {
const layerId = await app.addCogLayer?.(
"Satellite DEM",
"https://example.com/dem.tif",
{ colormap: "terrain", opacity: 0.8 }
);
const maplibre = await import("maplibre-gl");
navControl = new maplibre.NavigationControl();
app.addMapControl?.(navControl, "top-left");
},
deactivate(app: GeoLibreAppAPI) {
if (navControl) {
app.removeMapControl?.(navControl);
navControl = null;
}
}
};
From docs/plugin-api.md#app-addcoglayer.
Example 3: Custom Sidebar Panel
import type { GeoLibreAppAPI, GeoLibrePlugin } from "@geolibre/plugins";
export const workbench: GeoLibrePlugin = {
id: "workbench",
name: "Workbench",
version: "1.0.0",
private _cleanup?: () => void,
activate(app: GeoLibreAppAPI) {
this._cleanup = app.registerRightPanel?.({
id: "workbench",
title: "Workbench",
defaultWidth: 300,
render(container) {
const btn = document.createElement("button");
btn.textContent = "Run analysis";
btn.onclick = () => console.log("Analysis executed");
container.appendChild(btn);
return () => btn.remove();
}
});
app.openRightPanel?.("workbench");
},
deactivate(app: GeoLibreAppAPI) {
app.closeRightPanel?.("workbench");
this._cleanup?.();
}
};
From docs/plugin-api.md#app-registerrightpanel.
Key Source Files for Plugin Development
| File | Purpose |
|---|---|
docs/plugin-api.md |
Canonical API specification—all interfaces and types |
packages/plugins/src/plugin-manager.ts |
PluginManager implementation |
apps/geolibre-desktop/src/lib/plugin-registry.ts |
PluginRegistryEntry definition |
apps/geolibre-desktop/src/lib/plugin-integrity.ts |
External plugin validation |
apps/geolibre-desktop/src/lib/plugin-trust.ts |
Sandboxing and permissions |
apps/geolibre-desktop/src/hooks/usePlugins.ts |
Startup loading logic |
tests/plugin-manager.test.ts |
Lifecycle behavior verification |
Summary
- The
GeoLibrePlugininterface defines your plugin's identity and lifecycle hooks—implementactivate()anddeactivate()as the foundation - The
GeoLibreAppAPIobject exposes 30+ methods across map control, layer management, UI registration, and plugin coordination - All API methods are optional—use optional chaining (
app.method?.()) for defensive coding across host variants - Cloud-native formats (COG, Zarr) have dedicated methods with Promise-based async signatures
- UI registration uses plain DOM—no React dependency required, just return cleanup functions from
render() - Cross-plugin activation and project state persistence enable composable, multi-plugin workflows
Frequently Asked Questions
What is the minimum viable GeoLibre plugin?
A valid plugin requires only id, name, version, and an activate method. The deactivate hook is optional but strongly recommended for resource cleanup. All other capabilities—URL handling, state persistence, UI registration—are opt-in through additional properties and method calls.
Can plugins access the raw MapLibre GL map instance?
Yes. Call app.getMap() to retrieve the underlying Map instance, or null if the map isn't ready. Use this for advanced operations not covered by the high-level API, but note that direct map manipulation bypasses GeoLibre's state synchronization—consider registerExternalNativeLayer for custom layers that need UI integration.
How do plugins handle project file persistence?
Call app.getProjectState() during deactivate() to capture your plugin's settings, then return them in the state object. On project load, GeoLibre calls activate() and your plugin can read the restored state from app.getProjectState(). The .geolibre file format stores plugin state under each plugin's id key.
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 →