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

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.

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, 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 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, 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), which provides methods to manipulate the map, UI, and data layers.

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, and import it into 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 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:

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:

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:

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 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 or distribute external plugins as zip files with 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 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 manifest using npm run package:geolibre, and install via the GeoLibre Settings UI.

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 →