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

> Learn to create a custom plugin for GeoLibre with the Plugin API. Implement GeoLibrePlugin interface and register with PluginManager to extend GeoLibre functionality.

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

---

**To create a custom plugin for GeoLibre, implement the `GeoLibrePlugin` interface with required metadata fields and lifecycle methods (`activate` and `deactivate`), then register your plugin through the `PluginManager` located in [`packages/plugins/src/plugin-manager.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/plugin-manager.ts).**

GeoLibre is an open-source geospatial visualization platform built on MapLibre GL JS. Its **Plugin API** enables developers to extend core functionality—adding map controls, custom panels, toolbar menus, and data layers—without modifying the underlying source code. This guide walks through the complete process of building, registering, and deploying plugins using the official type definitions and host API contracts.

## Understanding the GeoLibre Plugin Architecture

The plugin system centers on two core components: the interface definition and the runtime manager.

In [`packages/plugins/src/types.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/types.ts), the `GeoLibrePlugin` interface defines the contract every plugin must satisfy. At minimum, your plugin object must include:

- **`id`**: A unique string identifier
- **`name`**: Human-readable display name  
- **`version`**: Semantic version string
- **`activate(app)`**: Lifecycle hook receiving `GeoLibreAppAPI` to interact with the host
- **`deactivate(app)`**: Cleanup hook to remove UI elements and listeners

The `PluginManager` class in [`packages/plugins/src/plugin-manager.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/plugin-manager.ts) handles the runtime lifecycle, invoking `activate()` when users enable your plugin and `deactivate()` when disabled. For built-in plugins, registration occurs in [`apps/geolibre-desktop/src/hooks/usePlugins.ts`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/hooks/usePlugins.ts), while external plugins are loaded dynamically via manifest URLs.

## Step-by-Step: Creating Your First Custom Plugin

### 1. Implement the GeoLibrePlugin Interface

Create a TypeScript module that exports a `GeoLibrePlugin` object. The `activate()` method receives the `GeoLibreAppAPI` (full contract documented in [`docs/plugin-api.md`](https://github.com/opengeos/GeoLibre/blob/main/docs/plugin-api.md)), which provides methods to manipulate the map, UI, and data layers.

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

export const myPlugin: GeoLibrePlugin = {
  id: "my-custom-plugin",
  name: "My Custom Plugin",
  version: "1.0.0",
  
  activate(app: GeoLibreAppAPI) {
    // Plugin initialization logic here
  },
  
  deactivate(app: GeoLibreAppAPI) {
    // Cleanup logic here
  }
};

```

### 2. Register Your Plugin

**For built-in plugins:** Place your 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 import it into [`apps/geolibre-desktop/src/hooks/usePlugins.ts`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/hooks/usePlugins.ts) to register with the `PluginManager`.

**For external plugins:** Package your compiled bundle as a zip file containing a [`plugin.json`](https://github.com/opengeos/GeoLibre/blob/main/plugin.json) manifest. Users install these via Settings > Manage Plugins, or you can host the manifest URL for direct loading. The external loader uses the same `PluginManager` to load and activate at runtime.

### 3. Interact with the Host Application

Inside `activate(app)`, leverage the `GeoLibreAppAPI` to extend functionality:

- **Basemap control**: `app.setBasemap(styleUrl)` switches map styles
- **Vector layers**: `app.addGeoJsonLayer(name, geojson)` registers GeoJSON sources
- **Raster layers**: `app.addTileLayer(name, url, options)` for XYZ tiles, or `app.addCogLayer(name, url, options)` for Cloud Optimized GeoTIFFs
- **Zarr support**: `app.addZarrLayer(name, url, opts)` loads cloud-native Zarr datasets
- **Map controls**: `app.addMapControl(control, position)` attaches MapLibre `IControl` instances
- **UI panels**: `app.registerRightPanel({...})` creates dockable sidebar panels, `app.registerFloatingPanel({...})` creates draggable overlays
- **Toolbar menus**: `app.registerToolbarMenu({...})` adds top-level application menus
- **Deck.gl integration**: `app.getDeckGL?.()` accesses shared Deck.gl modules for 3D rendering
- **Plugin orchestration**: `app.activatePlugin('id')` and `app.deactivatePlugin('id')` control other plugins

All API methods are optional (typed with `?`) to ensure forward compatibility across host variants.

### 4. Handle Cleanup in Deactivate

The `deactivate()` method must reverse all changes made during activation to prevent memory leaks and orphaned UI elements:

- Remove map controls using `app.removeMapControl(control)`
- Close and unregister panels via `app.closeRightPanel(id)` and the cleanup functions returned by registration methods
- Unregister custom layers with `app.unregisterExternalNativeLayer(id)`
- Clear timers, event listeners, and global state

## Complete Code Examples for GeoLibre Plugins

### Example 1: Simple Basemap Switcher

This minimal plugin switches to a custom map style when activated:

```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() {
    // Basemap persists until user changes it; no cleanup required
  },
};

```

### Example 2: Toolbar Menu with Right Sidebar Panel

This plugin creates a workbench panel accessible via both the right sidebar and a toolbar menu:

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

export const workbenchPlugin: GeoLibrePlugin = {
  id: "workbench",
  name: "Workbench",
  version: "0.1.0",
  
  activate(app: GeoLibreAppAPI) {
    // Register right sidebar panel
    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();
      },
    });

    // Open panel immediately
    app.openRightPanel?.("workbench");

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

    // Store cleanup functions
    (this as any)._cleanup = () => {
      unregisterPanel?.();
      unregisterMenu?.();
    };
  },
  
  deactivate(app: GeoLibreAppAPI) {
    app.closeRightPanel?.("workbench");
    (this as any)._cleanup?.();
  },
};

```

### Example 3: Custom MapLibre Control

Demonstrates adding a native MapLibre control and removing it on deactivation:

```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);
  },
};

```

## Building and Testing Your Plugin

Compile your plugin using the standard npm scripts:

- **`npm run build`**: Production build
- **`npm run dev`**: Hot-reloading development server
- **`npm run test:frontend`**: Run TypeScript type checking against the host API
- **`npm run package:geolibre`**: Generate distributable zip for external plugins (available via plugin template)

For external plugins, install the generated zip through the GeoLibre UI to verify runtime behavior.

## Summary

- Implement the `GeoLibrePlugin` interface defined in [`packages/plugins/src/types.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/types.ts) with mandatory `id`, `name`, `version`, `activate()`, and `deactivate()` properties
- Access the host application capabilities through the `GeoLibreAppAPI` object passed to lifecycle methods
- Register built-in plugins in [`apps/geolibre-desktop/src/hooks/usePlugins.ts`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/hooks/usePlugins.ts) or distribute external plugins as zip files with [`plugin.json`](https://github.com/opengeos/GeoLibre/blob/main/plugin.json) manifests
- Clean up all UI elements, map controls, and event listeners in the `deactivate()` method to prevent memory leaks
- Use the `PluginManager` in [`packages/plugins/src/plugin-manager.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/plugin-manager.ts) to handle plugin lifecycle and state management

## Frequently Asked Questions

### What is the minimum code required to create a valid GeoLibre plugin?

At minimum, you must export an object satisfying the `GeoLibrePlugin` interface with a unique `id`, `name`, `version`, and the `activate(app)` method. The `deactivate()` method is strongly recommended for cleanup but technically optional depending on your plugin's side effects.

### How do I add a custom button to the GeoLibre toolbar?

Use the `app.registerToolbarMenu()` method inside your `activate()` function. Pass a configuration object with an `id`, `label`, and `items` array containing your button actions. Store the returned cleanup function and call it during `deactivate()` to remove the menu.

### Can my plugin interact with other installed plugins?

Yes. The `GeoLibreAppAPI` provides `activatePlugin('plugin-id')` and `deactivatePlugin('plugin-id')` methods that allow your plugin to programmatically control other plugins. You can also check plugin states through the `PluginManager` if you have access to the app instance.

### Where should I place my plugin code for development versus production?

During development, place built-in plugins under `packages/plugins/src/plugins/` for integration with the monorepo's hot-reloading. For production distribution as an external plugin, compile to a standalone bundle, package it with a [`plugin.json`](https://github.com/opengeos/GeoLibre/blob/main/plugin.json) manifest using `npm run package:geolibre`, and install via the GeoLibre Settings UI.