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

> Develop a custom GeoLibre plugin using the Plugin Interface. Implement GeoLibrePlugin, register with PluginManager, and extend functionality without altering core code.

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

---

**Create a GeoLibre custom plugin by implementing the `GeoLibrePlugin` interface, registering it with the `PluginManager`, and using the `GeoLibreAppAPI` to add map controls, panels, and layers without modifying core code.**

The **GeoLibre plugin interface**, defined in [`packages/plugins/src/types.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/types.ts), provides a standardized contract for extending GeoLibre's mapping capabilities. Plugins run through the `PluginManager` in [`packages/plugins/src/plugin-manager.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/plugin-manager.ts), enabling safe activation and deactivation of third-party functionality. This guide walks through the complete process—from interface implementation to cleanup handlers—using patterns directly from the opengeos/GeoLibre source code.

## Understanding the GeoLibrePlugin Interface

Every custom plugin must satisfy the `GeoLibrePlugin` type definition. The interface requires three identification properties and two lifecycle methods.

### Required Plugin Properties

- `id` – Unique string identifier (kebab-case recommended)
- `name` – Human-readable display name
- `version` – Semantic version string

### Required Lifecycle Methods

- `activate(app: GeoLibreAppAPI)` – Called when the plugin is enabled; use this to register UI elements, layers, and event listeners
- `deactivate(app: GeoLibreAppAPI)` – Called when the plugin is disabled; must remove all resources added during activation

The `GeoLibreAppAPI` type, documented in [`docs/plugin-api.md`](https://github.com/opengeos/GeoLibre/blob/main/docs/plugin-api.md), exposes methods for interacting with the map, sidebar panels, toolbars, and layer management.

## Step-by-Step: Building Your First Plugin

### Step 1: Create the Plugin Module

Start with a TypeScript file that imports the core types and exports a `GeoLibrePlugin` object. The simplest valid plugin switches the basemap when activated:

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

export const myBasemapPlugin: GeoLibrePlugin = {
  id: "my-custom-basemap",
  name: "My Custom Basemap",
  version: "1.0.0",
  
  activate(app: GeoLibreAppAPI) {
    app.setBasemap("https://example.com/styles/terrain.json");
  },
  
  deactivate() {
    // Basemap persists until user changes it; no cleanup required
  },
};

```

This pattern matches the sample code in [`docs/plugin-api.md`](https://github.com/opengeos/GeoLibre/blob/main/docs/plugin-api.md) lines 41-48. The `setBasemap` method immediately applies a new MapLibre style URL to the active map.

### Step 2: Register UI Components with the Host API

Most plugins add interactive elements. The `GeoLibreAppAPI` provides methods across several categories:

**Map controls** – `addMapControl(control, position?)` and `removeMapControl(control)`
**Sidebar panels** – `registerRightPanel(config)`, `openRightPanel(id)`, `closeRightPanel(id)`
**Toolbar menus** – `registerToolbarMenu(config)`
**Floating panels** – `registerFloatingPanel(config)`
**Layer registration** – `addGeoJsonLayer`, `addTileLayer`, `addCogLayer`, `addZarrLayer`
**External native layers** – `registerExternalNativeLayer` for direct MapLibre layer integration
**Deck.gl access** – `getDeckGL()` for 3D visualization

All API methods are optional (typed with `?`) to maintain forward compatibility with host variants that may not expose every feature.

### Step 3: Handle Cleanup in Deactivate

The `deactivate` method prevents memory leaks and orphaned UI elements. Store references to registered components during activation, then remove them during deactivation:

```typescript
activate(app: GeoLibreAppAPI) {
  // Register components and store cleanup functions
  const unregisterPanel = app.registerRightPanel?.({...});
  const unregisterMenu = app.registerToolbarMenu?.({...});
  
  // Store for later cleanup
  this._cleanup = () => {
    unregisterPanel?.();
    unregisterMenu?.();
  };
},

deactivate(app: GeoLibreAppAPI) {
  // Execute all cleanup functions
  this._cleanup?.();
  app.closeRightPanel?.("my-panel-id");
}

```

The `PluginManager` in [`packages/plugins/src/plugin-manager.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/plugin-manager.ts) guarantees `deactivate` is called before a plugin is removed, even during application shutdown or plugin replacement.

## Complete Examples: Common Plugin Patterns

### Example: Toolbar Menu with Right Sidebar Panel

This pattern combines a top-level menu item with a dockable sidebar panel, following the implementation style in [`docs/plugin-api.md`](https://github.com/opengeos/GeoLibre/blob/main/docs/plugin-api.md) lines 443-466:

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

export const analysisWorkbenchPlugin: GeoLibrePlugin = {
  id: "analysis-workbench",
  name: "Analysis Workbench",
  version: "2.1.0",

  activate(app: GeoLibreAppAPI) {
    // Register right sidebar panel
    const unregisterPanel = app.registerRightPanel?.({
      id: "workbench-panel",
      title: "Analysis Workbench",
      defaultWidth: 400,
      icon: "🔬",
      render(container) {
        const header = document.createElement("h3");
        header.textContent = "Available Tools";
        
        const list = document.createElement("ul");
        ["Buffer", "Intersect", "Union"].forEach(tool => {
          const li = document.createElement("li");
          li.textContent = tool;
          list.appendChild(li);
        });
        
        container.appendChild(header);
        container.appendChild(list);
        
        // Return cleanup function for this render
        return () => {
          container.innerHTML = "";
        };
      },
    });

    // Register toolbar menu with action
    const unregisterMenu = app.registerToolbarMenu?.({
      id: "workbench-menu",
      label: "Analysis",
      icon: "🔬",
      items: [
        {
          id: "open-workbench",
          label: "Open Workbench",
          onSelect: () => app.openRightPanel?.("workbench-panel"),
        },
        {
          id: "run-quick-buffer",
          label: "Quick Buffer (100m)",
          onSelect: () => {
            // Execute analysis directly
            console.log("Running 100m buffer...");
          },
        },
      ],
    });

    // Open panel immediately on plugin activation
    app.openRightPanel?.("workbench-panel");

    // Store cleanup functions
    (this as any)._dispose = () => {
      unregisterPanel?.();
      unregisterMenu?.();
    };
  },

  deactivate(app: GeoLibreAppAPI) {
    app.closeRightPanel?.("workbench-panel");
    (this as any)._dispose?.();
  },
};

```

### Example: Custom MapLibre Control

Add native MapLibre controls using the optional control API from [`docs/plugin-api.md`](https://github.com/opengeos/GeoLibre/blob/main/docs/plugin-api.md) lines 109-113:

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

export const enhancedControlsPlugin: GeoLibrePlugin = {
  id: "enhanced-controls",
  name: "Enhanced Map Controls",
  version: "1.0.0",
  
  _controls: [] as maplibregl.IControl[],

  activate(app: GeoLibreAppAPI) {
    const scale = new ScaleControl({ maxWidth: 200, unit: "metric" });
    const fullscreen = new FullscreenControl();
    
    app.addMapControl(scale, "bottom-left");
    app.addMapControl(fullscreen, "top-right");
    
    this._controls = [scale, fullscreen];
  },

  deactivate(app: GeoLibreAppAPI) {
    this._controls.forEach(control => {
      app.removeMapControl?.(control);
    });
    this._controls = [];
  },
};

```

### Example: Layer Registration with Persistence

Register custom layers that appear in GeoLibre's Layers panel and participate in state persistence:

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

export const seismicDataPlugin: GeoLibrePlugin = {
  id: "seismic-data",
  name: "Real-time Seismic Data",
  version: "1.2.0",
  
  _layerId: null as string | null,
  _pollInterval: null as number | null,

  async activate(app: GeoLibreAppAPI) {
    // Fetch and display GeoJSON layer
    const response = await fetch("https://api.example.com/earthquakes.geojson");
    const geojson = await response.json();
    
    this._layerId = app.addGeoJsonLayer("Recent Earthquakes", geojson, {
      cluster: true,
      clusterMaxZoom: 14,
      paint: {
        "circle-color": "#ff4500",
        "circle-radius": ["step", ["get", "mag"], 4, 5, 8, 7, 12],
      },
    });

    // Set up polling for updates
    this._pollInterval = window.setInterval(async () => {
      const fresh = await fetch("https://api.example.com/earthquakes.geojson");
      // Layer update logic would go here
    }, 60000);
  },

  deactivate(app: GeoLibreAppAPI) {
    if (this._pollInterval) {
      clearInterval(this._pollInterval);
    }
    // Layer is automatically removed when plugin deactivates
    // For manual cleanup: app.removeLayer?.(this._layerId)
  },
};

```

## Registering Your Plugin: Built-in vs. External

### Built-in Plugin Registration

For plugins shipped with GeoLibre, follow this file structure:

1. Create source file in [`packages/plugins/src/plugins/your-plugin.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/plugins/your-plugin.ts)
2. Export from [`packages/plugins/src/index.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/index.ts)
3. Register in [`apps/geolibre-desktop/src/hooks/usePlugins.ts`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/hooks/usePlugins.ts)

The [`usePlugins.ts`](https://github.com/opengeos/GeoLibre/blob/main/usePlugins.ts) hook initializes the `PluginManager` and loads built-in plugins at application startup. Example registration pattern:

```typescript
import { myBasemapPlugin } from "@geolibre/plugins";

// Inside usePlugins hook
pluginManager.registerBuiltIn(myBasemapPlugin);

```

### External Plugin Distribution

For independently distributed plugins:

1. Compile to JavaScript bundle
2. Create [`plugin.json`](https://github.com/opengeos/GeoLibre/blob/main/plugin.json) manifest with metadata matching your `GeoLibrePlugin` identifiers
3. Package as zip archive
4. Install via **Settings > Manage Plugins** in GeoLibre desktop, or host manifest URL for network installation

The `PluginManager` loads external plugins through the same activation/deactivation flow, sandboxing them from core application code.

## Build, Test, and Package

GeoLibre provides tooling for each development phase:

- **Development**: `npm run dev` starts hot-reloading dev server
- **Type checking**: `npm run test:frontend` validates TypeScript types against host API
- **Production build**: `npm run build` compiles optimized plugin bundle
- **Packaging**: `npm run package:geolibre` (from plugin template) generates installable zip with manifest

Run integration tests by installing the packaged plugin through the GeoLibre UI and verifying activation, functionality, and clean deactivation.

## Key Source Files for Plugin Development

| File | Purpose |
|------|---------|
| [`packages/plugins/src/types.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/types.ts) | `GeoLibrePlugin` and `GeoLibreAppAPI` type definitions |
| [`packages/plugins/src/plugin-manager.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/plugin-manager.ts) | Core runtime for plugin lifecycle management |
| [`apps/geolibre-desktop/src/hooks/usePlugins.ts`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/hooks/usePlugins.ts) | Built-in plugin registration boilerplate |
| [`docs/plugin-api.md`](https://github.com/opengeos/GeoLibre/blob/main/docs/plugin-api.md) | Complete API reference with all optional methods |
| [`docs/user-guide/plugins.md`](https://github.com/opengeos/GeoLibre/blob/main/docs/user-guide/plugins.md) | End-user documentation for plugin installation |

## Summary

- **Implement the interface**: Create a TypeScript module exporting a `GeoLibrePlugin` object with `id`, `name`, `version`, `activate`, and `deactivate`
- **Use the host API**: Call methods on `GeoLibreAppAPI` to add map controls (`addMapControl`), sidebar panels (`registerRightPanel`), toolbar menus (`registerToolbarMenu`), and layers (`addGeoJsonLayer`, `addTileLayer`, etc.)
- **Clean up completely**: Remove all registered components in `deactivate` to prevent memory leaks and orphaned UI elements
- **Register appropriately**: Add built-in plugins through [`usePlugins.ts`](https://github.com/opengeos/GeoLibre/blob/main/usePlugins.ts) or distribute external plugins as zip packages with [`plugin.json`](https://github.com/opengeos/GeoLibre/blob/main/plugin.json) manifests
- **Reference the source**: Type definitions in [`packages/plugins/src/types.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/types.ts) and the `PluginManager` implementation in [`packages/plugins/src/plugin-manager.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/plugin-manager.ts) define the exact contract your plugin must satisfy

## Frequently Asked Questions

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

A valid plugin requires five properties: `id`, `name`, `version`, `activate(app)`, and `deactivate(app)`. The simplest implementation provides empty lifecycle methods, though practical plugins use `activate` to register functionality with the `GeoLibreAppAPI`. See the basemap example in [`docs/plugin-api.md`](https://github.com/opengeos/GeoLibre/blob/main/docs/plugin-api.md) lines 41-48 for a minimal working template.

### Can a plugin control other plugins?

Yes. The `GeoLibreAppAPI` includes optional lifecycle helpers `activatePlugin(otherId)` and `deactivatePlugin(otherId)`. These methods enable plugin orchestration—for example, a "Survey Mode" plugin that automatically activates measurement tools and layer visibility controls. All API methods are optional (`?`) typed to ensure forward compatibility.

### How do I persist plugin state between sessions?

GeoLibre handles persistence for registered layers, panels, and map state automatically. For custom plugin-specific data, use URL parameters (handled via optional plugin hooks) or the browser's `localStorage`/`IndexedDB`. The `registerExternalNativeLayer` method ensures direct MapLibre layers appear in the Layers panel and participate in built-in state serialization.

### What happens if my plugin throws an error during activation?

The `PluginManager` in [`packages/plugins/src/plugin-manager.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/plugin-manager.ts) wraps plugin activation in error handling. A failed activation marks the plugin as errored and prevents it from running, but does not crash the host application. Check the browser console and GeoLibre's internal logs for stack traces. Defensive coding in `activate`—checking for optional API methods with `?.`—prevents common failures.