# How to Create and Register a Custom Plugin in GeoLibre: A Complete Guide

> Create and register custom GeoLibre plugins easily. Implement the GeoLibrePlugin interface and use PluginManager to add your functionality. Get started today!

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

---

**To create and register a custom plugin in GeoLibre, implement the `GeoLibrePlugin` interface with required `id`, `activate`, and `deactivate` methods, then pass the plugin instance to `PluginManager.register()`.**

GeoLibre provides a store-driven, extensible architecture that allows developers to add functionality through a formal plugin system. The framework centers on two core components: the `GeoLibrePlugin` interface, which defines the plugin contract, and the `PluginManager` class, which handles registration, lifecycle management, and state persistence. This guide walks through the exact implementation patterns used in the opengeos/GeoLibre repository, referencing specific source files and method signatures.

## Understanding the GeoLibre Plugin Architecture

At the heart of GeoLibre’s extensibility lies a strict interface-based design. The **`GeoLibrePlugin`** interface, defined in [[`packages/plugins/src/types.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/types.ts)](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/types.ts), mandates that every plugin expose a unique identifier and lifecycle hooks. The **`PluginManager`** class, implemented in [[`packages/plugins/src/plugin-manager.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/plugin-manager.ts)](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/plugin-manager.ts), maintains an internal `Map<string, GeoLibrePlugin>` registry and orchestrates activation flows.

Plugins receive a **scoped app API** (`GeoLibreAppAPI`) during activation. This scoped API ensures that UI elements—such as map controls and toolbar menus—are correctly tagged with plugin ownership, preventing conflicts and enabling automatic cleanup when the plugin deactivates.

## Step 1: Implement the GeoLibrePlugin Interface

To create a custom plugin in GeoLibre, you must define an object that conforms to the `GeoLibrePlugin` interface. This contract ensures the host application can consistently initialize, run, and tear down your extension.

### Required Plugin Fields

Every plugin must provide three essential properties:

- **`id`**: A unique string identifier used as the key in the manager’s registry.
- **`activate`**: A function receiving `GeoLibreAppAPI` that runs when the plugin is turned on. It may return `boolean` or `Promise<boolean>` to indicate success or failure.
- **`deactivate`**: A function receiving `GeoLibreAppAPI` that cleans up resources when the plugin is turned off.

### Optional Lifecycle Callbacks

The interface also defines optional hooks for deeper integration:

- **`activeByDefault`**: Boolean indicating whether the plugin should activate automatically when a project loads.
- **`handleUrlParameters`**: Callback for parsing query strings on app startup.
- **`getMapControlPosition` / `setMapControlPosition`**: Methods for managing map control layouts.
- **`applyProjectState`**: Hook for restoring plugin-specific state from saved projects.

Here is a complete implementation example:

```typescript
// src/custom-plugins/myPlugin.ts
import type { GeoLibrePlugin, GeoLibreAppAPI } from '@geolibre/plugins';

export const myPlugin: GeoLibrePlugin = {
  id: 'my-plugin',
  activeByDefault: true,

  activate: (app: GeoLibreAppAPI) => {
    // Create a custom map control
    const ctrl = {
      onAdd: () => {
        const el = document.createElement('div');
        el.textContent = 'My Plugin 🎉';
        el.style.cssText = 'padding:4px;background:#fff;border-radius:4px;';
        return el;
      },
      onRemove: () => {},
    };

    // Register the control using the scoped API
    app.addMapControl(ctrl, 'top-right');
    return true; // Synchronous activation success
  },

  deactivate: (app: GeoLibreAppAPI) => {
    // Clean up the control added during activation
    app.removeMapControl?.('my-plugin-control');
  },

  registerToolbarMenu: (menu) => {
    // Menu registration is automatically scoped to this plugin
    // via the `scopeAppToPlugin` helper in plugin-manager.ts
  },
};

export default myPlugin;

```

## Step 2: Register Your Plugin with PluginManager

Once you have defined your plugin object, you must register it with the **`PluginManager`** instance. The manager exposes two primary methods for adding plugins to the registry:

- **`register(plugin)`**: Adds a single `GeoLibrePlugin` to the internal `Map<string, GeoLibrePlugin>`.
- **`registerAll(plugins)`**: Accepts an array of plugins and registers each atomically.

In a typical GeoLibre application, the entry point creates a single `PluginManager` instance and stores it in the global state (often a Zustand store in `@geolibre/core`). You then call the registration methods before the app renders:

```typescript
// src/main.ts
import { PluginManager } from '@geolibre/plugins';
import { myPlugin } from './custom-plugins/myPlugin';

// Instantiate the manager once per application lifecycle
const pluginManager = new PluginManager();

// Register a single custom plugin
pluginManager.register(myPlugin);

// Or register multiple plugins at once
// pluginManager.registerAll([myPlugin, analyticsPlugin, dataImportPlugin]);

// Expose the manager through your React context or state store
// so UI components can subscribe to plugin state changes

```

## Step 3: Handle Activation and Deactivation

The **`PluginManager.activate(id, app)`** method triggers the plugin lifecycle. When called, the manager looks up the plugin by its `id` in the internal registry and invokes the `activate` method, passing the scoped `GeoLibreAppAPI`.

If `activate` returns a `Promise`, the manager monitors its resolution:

- If the promise resolves to `true`, the plugin state updates to active.
- If the promise resolves to `false` or rejects, the manager rolls back the activation and keeps the plugin inactive.

Deactivation follows a symmetric path through **`PluginManager.deactivate(id, app)`**, which calls the plugin’s `deactivate` hook to release resources and remove UI components.

## Integration Points and the Scoped App API

The **`GeoLibreAppAPI`** object passed to lifecycle hooks is the primary mechanism for extending GeoLibre’s UI and map functionality. This API is scoped to your plugin via the `scopeAppToPlugin` helper in [`plugin-manager.ts`](https://github.com/opengeos/GeoLibre/blob/main/plugin-manager.ts), ensuring proper ownership tracking.

### Adding Map Controls

Use `app.addMapControl(control, position)` to inject custom DOM elements into the map canvas. The control object must implement `onAdd()` and `onRemove()` methods. Positions follow the MapLibre GL convention: `'top-left'`, `'top-right'`, `'bottom-left'`, or `'bottom-right'`.

### Registering Toolbar Menus

The optional `registerToolbarMenu` callback receives a menu configuration object following the `GeoLibreToolbarMenu` shape. Because the API is pre-scoped, the framework automatically tags these UI elements with your plugin’s `id`, allowing the manager to disable them if the plugin is deactivated.

## Loading External Plugins (Drop-in Extensions)

GeoLibre supports external plugins without recompiling the core application. Place a zip file containing your bundled code and a [`plugin.json`](https://github.com/opengeos/GeoLibre/blob/main/plugin.json) manifest in `apps/geolibre-desktop/public/plugins/`. The **`usePlugins`** hook (typically found in [`apps/geolibre-desktop/src/hooks/usePlugins.ts`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/hooks/usePlugins.ts)) discovers these manifests, dynamically imports the modules, and registers them with the same `PluginManager` used for built-in plugins.

## Summary

- **Implement `GeoLibrePlugin`**: Define `id`, `activate`, and `deactivate` in an object conforming to the interface in [`packages/plugins/src/types.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/types.ts).
- **Register via `PluginManager`**: Call `register()` or `registerAll()` on the manager instance defined in [`packages/plugins/src/plugin-manager.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/plugin-manager.ts).
- **Leverage the scoped API**: Use `GeoLibreAppAPI` to add map controls and toolbar menus; the manager handles ownership and cleanup automatically.
- **Handle async activation**: Return a boolean or Promise from `activate()`; the manager rolls back on `false` or rejection.
- **Deploy externally**: Package plugins as zips with [`plugin.json`](https://github.com/opengeos/GeoLibre/blob/main/plugin.json) manifests for drop-in installation without code changes.

## Frequently Asked Questions

### What interface must a GeoLibre plugin implement?

Every custom plugin must implement the **`GeoLibrePlugin`** interface declared in [`packages/plugins/src/types.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/types.ts). This requires an `id` string, an `activate` method receiving `GeoLibreAppAPI`, and a `deactivate` method for cleanup. Optional fields like `activeByDefault` and `handleUrlParameters` enable deeper integration with the host application.

### How does PluginManager handle asynchronous plugin activation?

When `PluginManager.activate(id, app)` invokes a plugin’s `activate` method, it checks the return type. If the method returns a `Promise<boolean>`, the manager awaits resolution. A resolved value of `true` commits the activation, while `false` or a rejected promise triggers a rollback, keeping the plugin inactive and preventing partial state corruption.

### Can I load external plugins without modifying the core codebase?

Yes. GeoLibre supports drop-in plugins through the **`usePlugins`** hook. Bundle your plugin as a zip file with a [`plugin.json`](https://github.com/opengeos/GeoLibre/blob/main/plugin.json) manifest and place it in `apps/geolibre-desktop/public/plugins/`. The hook dynamically imports your code and registers it with the existing `PluginManager`, requiring no changes to the core repository.

### Where does GeoLibre store the active state of plugins?

The `PluginManager` maintains plugin state in an internal **`Map<string, GeoLibrePlugin>`** and exposes a `subscribe` method for reactive UI updates. For persistence across sessions, the manager serializes active plugin IDs and their states into the project file, restoring them via `applyProjectState` callbacks when the project reloads.