# How to Implement the Plugin API for Third-Party GeoLibre Extensions

> Learn to implement the GeoLibre plugin API to extend functionality. Build custom map controls, layers, and UI panels with @geolibre/plugins and PluginManager.

- Repository: [Open Geospatial Solutions/GeoLibre](https://github.com/opengeos/GeoLibre)
- Tags: how-to-guide
- Published: 2026-08-02

---

**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`](https://github.com/opengeos/GeoLibre/blob/main/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`](https://github.com/opengeos/GeoLibre/blob/main/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`](https://github.com/opengeos/GeoLibre/blob/main/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`](https://github.com/opengeos/GeoLibre/blob/main/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.

```typescript
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.

### Right-Sidebar Panels

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】.

```typescript
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】.

```typescript
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`](https://github.com/opengeos/GeoLibre/blob/main/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`](https://github.com/opengeos/GeoLibre/blob/main/docs/plugin-api.md)【L39-L80】.

### External Plugins

Distributed as ZIP bundles containing a [`plugin.json`](https://github.com/opengeos/GeoLibre/blob/main/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:

```typescript
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`](https://github.com/opengeos/GeoLibre/blob/main/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`](https://github.com/opengeos/GeoLibre/blob/main/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`](https://github.com/opengeos/GeoLibre/blob/main/plugin.json) manifest specifies the correct entry file path and that all dependencies are bundled or available as globals.