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 plugin
  • name: A human-readable display name
  • version: Semantic versioning string
  • activate(app): A lifecycle hook receiving the GeoLibreAppAPI instance where you initialize controls and UI
  • deactivate(app): A cleanup hook to remove listeners and UI components
  • handleUrlParameters: (Optional) Called when URL parameters declared in urlParameterNames are 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, and registerExternalNativeLayer for custom WebGL layers
  • UI registration: registerRightPanel, registerFloatingPanel, and registerToolbarMenu
  • Map controls: addMapControl and removeMapControl for 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.registerRightPanel or app.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.

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 with id, name, version, activate, and deactivate hooks as defined in docs/plugin-api.md【L26-L46】.
  • Use GeoLibreAppAPI: Access the host surface through the app parameter to register layers, controls, and UI panels with full TypeScript support【L58-L155】.
  • Manage Lifecycle: Store cleanup functions during activate and invoke them during deactivate to 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 handleUrlParameters and declare urlParameterNames to 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:

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 →