# How to Create Custom Plugins for GeoLibre Using the Plugin API

> Learn to create custom plugins for GeoLibre using its Plugin API. Extend mapping functionality with new controls, panels, and layers without modifying the core codebase.

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

---

**GeoLibre's Plugin API lets developers extend the mapping application with new controls, panels, layers, and toolbar menus by implementing the `GeoLibrePlugin` interface and registering the module with the `PluginManager`, all without touching the core codebase.**

The open-source `opengeos/GeoLibre` project is built around runtime extensibility, and its plugin architecture lets you inject functionality cleanly into both the desktop and web viewers. When you create custom plugins for GeoLibre using the plugin API, you interact with the host application through a typed `GeoLibreAppAPI` surface and lifecycle hooks.

## Define a GeoLibre Plugin

A valid plugin is a TypeScript module that exports an object satisfying the `GeoLibrePlugin` interface declared in [`packages/plugins/src/types.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/types.ts). At minimum, the object must supply three identifiers and two lifecycle hooks:

- **`id`**, **`name`**, and **`version`** — Unique strings used by the `PluginManager` to track the plugin.
- **`activate(app: GeoLibreAppAPI)`** — Called when the plugin is enabled; use this hook to register UI elements, layers, and event listeners via the host API.
- **`deactivate(app: GeoLibreAppAPI)`** — Called when the plugin is disabled; use this hook to tear down every resource created in `activate`.

The full type contract and optional hooks—such as URL-parameter handling and state persistence—are documented in [`docs/plugin-api.md`](https://github.com/opengeos/GeoLibre/blob/main/docs/plugin-api.md).

The snippet below shows the simplest possible plugin: a custom basemap switcher.

```typescript
import type { GeoLibreAppAPI, GeoLibrePlugin } from "@geolibre/plugins";

export const myBasemap: GeoLibrePlugin = {
  id: "my-basemap",
  name: "My Basemap",
  version: "0.1.0",
  activate(app: GeoLibreAppAPI) {
    app.setBasemap("https://example.com/styles/custom.json");
  },
  deactivate() {
    // No cleanup required for a simple basemap switch.
  },
};

```

## Register Your Plugin with the PluginManager

Registration paths differ for **built-in** plugins shipped with GeoLibre and **external** plugins distributed independently. The `PluginManager` implementation in [`packages/plugins/src/plugin-manager.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/plugin-manager.ts) handles activation and deactivation for both.

### Register a Built-In Plugin

Place the source file under `packages/plugins/src/plugins/`, export it from [`packages/plugins/src/index.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/index.ts), and wire it into the desktop application via [`apps/geolibre-desktop/src/hooks/usePlugins.ts`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/hooks/usePlugins.ts). This pattern lets core contributors maintain plugins inside the monorepo while the `PluginManager` still governs their lifecycle.

### Distribute an External Plugin

External plugins are compiled into a zip archive containing a [`plugin.json`](https://github.com/opengeos/GeoLibre/blob/main/plugin.json) manifest. Users install them through **Settings > Manage Plugins** in the GeoLibre UI, or by pointing to a hosted manifest URL. The external loader delegates to the same `PluginManager` used by built-in plugins, as described in [`docs/user-guide/plugins.md`](https://github.com/opengeos/GeoLibre/blob/main/docs/user-guide/plugins.md).

## Use the GeoLibreAppAPI to Extend the Map and UI

Inside `activate`, the `app` argument exposes the `GeoLibreAppAPI` surface defined in [`packages/plugins/src/types.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/types.ts). Because every method is typed as optional with `?`, you should invoke capabilities with optional chaining to remain forward-compatible with host variants that may not expose every feature.

Key host capabilities include:

- **Basemap control** — `app.setBasemap(styleUrl)` switches the map style.
- **Vector layers** — `app.addGeoJsonLayer(name, geojson)` registers GeoJSON sources that appear in the Layers panel.
- **Raster layers** — `app.addTileLayer(name, url, options?)` and `app.addCogLayer(name, url, options?)` add XYZ, WMTS, WMS, or COG raster layers.
- **Zarr support** — `app.addZarrLayer(name, url, opts)` loads cloud-native Zarr datasets.
- **Map controls** — `app.addMapControl(control, position?)` attaches a MapLibre `IControl`; `app.removeMapControl(control)` detaches it.
- **Right-sidebar panels** — `app.registerRightPanel({...})` creates a dockable panel, while `app.openRightPanel(id)` and `app.closeRightPanel(id)` manage visibility.
- **Toolbar menus** — `app.registerToolbarMenu({...})` adds a top-level menu with actions and submenus.
- **Floating panels** — `app.registerFloatingPanel({...})` creates draggable overlay panels.
- **Native layer persistence** — `app.registerExternalNativeLayer({...})` and `app.unregisterExternalNativeLayer(...)` integrate directly added MapLibre layers into the Layers panel.
- **Deck.gl integration** — `app.getDeckGL?.()` returns the shared Deck.gl context for high-performance 3D rendering.
- **Cross-plugin control** — `app.activatePlugin('other-id')` and `app.deactivatePlugin('other-id')` toggle other plugins from within your own.

The following example registers a toolbar menu that opens a right-sidebar panel.

```typescript
import type { GeoLibreAppAPI, GeoLibrePlugin } from "@geolibre/plugins";

export const workbenchPlugin: GeoLibrePlugin = {
  id: "workbench",
  name: "Workbench",
  version: "0.1.0",
  activate(app: GeoLibreAppAPI) {
    const unregisterPanel = app.registerRightPanel?.({
      id: "workbench",
      title: "Workbench",
      defaultWidth: 360,
      render(container) {
        const btn = document.createElement("button");
        btn.textContent = "Run analysis";
        container.appendChild(btn);
        return () => btn.remove();
      },
    });

    app.openRightPanel?.("workbench");

    const unregisterMenu = app.registerToolbarMenu?.({
      id: "workbench-menu",
      label: "Workbench",
      items: [
        {
          id: "open",
          label: "Open panel",
          onSelect: () => app.openRightPanel?.("workbench"),
        },
      ],
    });

    (this as any)._unregister = () => {
      unregisterPanel?.();
      unregisterMenu?.();
    };
  },
  deactivate(app: GeoLibreAppAPI) {
    app.closeRightPanel?.("workbench");
    (this as any)._unregister?.();
  },
};

```

## Clean Up Resources in the Deactivate Lifecycle Hook

Every plugin must implement `deactivate` to prevent memory leaks and orphan UI elements. According to the GeoLibre source code in [`packages/plugins/src/plugin-manager.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/plugin-manager.ts), you should reverse every mutation made during `activate`:

- Remove map controls with `app.removeMapControl`.
- Unregister panels and menus by calling the cleanup functions returned from `registerRightPanel`, `registerToolbarMenu`, and `registerFloatingPanel`.
- Close persistent panels with `app.closeRightPanel`.
- Unregister custom layers with `app.unregisterExternalNativeLayer`.
- Clear timers, event listeners, and any global state your plugin created.

The example below attaches a `NavigationControl` and then safely removes it.

```typescript
import type { GeoLibreAppAPI, GeoLibrePlugin } from "@geolibre/plugins";
import { NavigationControl } from "maplibre-gl";

export const navigationPlugin: GeoLibrePlugin = {
  id: "navigation-control",
  name: "Navigation Control",
  version: "0.1.0",
  activate(app: GeoLibreAppAPI) {
    const control = new NavigationControl();
    app.addMapControl(control, "top-left");
    (this as any)._control = control;
  },
  deactivate(app: GeoLibreAppAPI) {
    app.removeMapControl?.((this as any)._control);
  },
};

```

## Build and Test Your Plugin

GeoLibre provides standard npm scripts for compiling and validating plugins before release.

1. Run `npm run build` to produce a production bundle, or `npm run dev` to start a hot-reloading development server.
2. Verify type compatibility with the host API by running `npm run test:frontend`.
3. For external plugins, execute `npm run package:geolibre` using the plugin template to generate a zip archive. Install the resulting bundle through the GeoLibre UI to confirm runtime behavior.

## Summary

- GeoLibre plugins implement the `GeoLibrePlugin` interface defined in [`packages/plugins/src/types.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/types.ts), requiring `id`, `name`, `version`, `activate`, and `deactivate`.
- The `PluginManager` in [`packages/plugins/src/plugin-manager.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/plugin-manager.ts) orchestrates loading for both built-in plugins and external zip-based bundles.
- The `GeoLibreAppAPI` exposes methods for basemaps, vector and raster layers, map controls, panels, menus, floating panels, and Deck.gl integration.
- Always clean up controls, panels, layers, and listeners inside `deactivate` to avoid memory leaks.
- Use `npm run build`, `npm run dev`, and `npm run test:frontend` during development; package external plugins with `npm run package:geolibre`.

## Frequently Asked Questions

### What files define the GeoLibre plugin contract and runtime?

The `GeoLibrePlugin` and `GeoLibreAppAPI` interfaces are declared in [`packages/plugins/src/types.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/types.ts). The `PluginManager` class that handles registration, activation, and deactivation lives in [`packages/plugins/src/plugin-manager.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/plugin-manager.ts). Reference documentation is maintained in [`docs/plugin-api.md`](https://github.com/opengeos/GeoLibre/blob/main/docs/plugin-api.md).

### Can I build a plugin without modifying the GeoLibre core repository?

Yes. External plugins are compiled independently, packaged as a zip with a [`plugin.json`](https://github.com/opengeos/GeoLibre/blob/main/plugin.json) manifest, and installed at runtime through **Settings > Manage Plugins** or via a hosted manifest URL. The `PluginManager` loads them dynamically without requiring changes to GeoLibre's source code.

### How do I add a MapLibre custom control to GeoLibre?

Inside `activate`, instantiate your control and call `app.addMapControl(control, position)`. Save the control reference, then remove it during `deactivate` by calling `app.removeMapControl(control)`. This pattern is shown in [`docs/plugin-api.md`](https://github.com/opengeos/GeoLibre/blob/main/docs/plugin-api.md) and aligns with the standard MapLibre `IControl` interface.

### Which API methods are safe to call if the host application does not support every feature?

All `GeoLibreAppAPI` methods are typed as optional using `?`, so you should invoke them with optional chaining, for example `app.getDeckGL?.()`. This keeps your plugin forward-compatible with host variants that omit specific capabilities.