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 name
  • version: SemVer string for compatibility checks

Optional Properties

  • activeByDefault: Boolean for auto-activation on startup
  • urlParameterNames: 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 sources
  • addWmtsLayer(name, url, options?) — WMTS protocol
  • addWmsLayer(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) — Returns Promise<string>; renders v2/v3 Zarr stores
  • setZarrLayerSelector(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 paintBridge for 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:

  1. Built-in plugins load from packages/plugins/src/index.ts
  2. Bundled drop-ins load from public/plugins/
  3. 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.json manifest + compiled dist/*.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 GeoLibrePlugin interface defines your plugin's identity and lifecycle hooks—implement activate() and deactivate() as the foundation
  • The GeoLibreAppAPI object 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →