# GeoLibre Plugin API: How to Create Custom Plugins for Maps and Geospatial Data

> Learn to build custom plugins for maps and geospatial data using the GeoLibre plugin API. Gain full control over maps, layers, UI, and more with this typed JavaScript interface.

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

---

**GeoLibre's plugin API exposes a typed JavaScript interface that lets developers create custom plugins with full control over maps, layers, UI panels, toolbar menus, and cross-plugin coordination.**

The `GeoLibrePlugin` interface forms the core contract between the host application and your code. Whether you're building for desktop, web, or Jupyter environments, the same API surface gives you programmatic access to MapLibre GL, Deck.gl, cloud-optimized data formats, and the complete geospatial stack. This guide breaks down every available method with source-backed examples from the opengeos/GeoLibre repository.

## GeoLibrePlugin Interface: The Foundation

Every plugin must implement the `GeoLibrePlugin` interface defined in [`docs/plugin-api.md`](https://github.com/opengeos/GeoLibre/blob/main/docs/plugin-api.md). This contract specifies your plugin's identity, lifecycle, and optional capabilities.

### Required Properties

- `id`: Unique string identifier (kebab-case recommended)
- `name`: Human-readable display name
- `version`: SemVer string for compatibility checks

### Optional Properties

- `activeByDefault`: Boolean for auto-activation on startup
- `urlParameterNames`: Array of URL parameters this plugin handles

### Lifecycle Methods

```typescript
interface GeoLibrePlugin {
  activate(app: GeoLibreAppAPI): void | Promise<void>;
  deactivate(app: GeoLibreAppAPI): void | Promise<void>;
  handleUrlParameters?(app: GeoLibreAppAPI, params: Record<string, string>): void;
}

```

The host calls `activate()` when a user enables your plugin, passing a fully populated `GeoLibreAppAPI` instance. The `deactivate()` hook triggers on disable or unload—use it to clean up layers, panels, and event listeners.

## GeoLibreAppAPI: Complete Method Reference

The `GeoLibreAppAPI` object exposes 30+ methods across eight functional areas. All methods are optional (typed as possibly `undefined`), so defensive coding with optional chaining is recommended.

### Map and Basemap Control

| Method | Purpose |
|--------|---------|
| `setBasemap(styleUrl)` | Switch base style via MapLibre GL style URL |
| `getActiveBasemap()` | Retrieve current base style URL |
| `addMapControl(control, position?)` | Inject custom `IControl` at specified corner |
| `removeMapControl(control)` | Remove previously added control |
| `setBuiltInMapControlVisible(control, visible)` | Toggle native controls (navigation, fullscreen, terrain) |
| `setBuiltInMapControlPosition(control, position)` | Relocate built-in controls |

**Position values**: `top-left`, `top-right`, `bottom-left`, `bottom-right` from the `GeoLibreMapControlPosition` enum.

### Layer Management: Vector, Raster, and Cloud-Native

**Vector data**

- `addGeoJsonLayer(name, data, sourcePath?)` — Registers GeoJSON as a native layer with automatic UI panel integration and project persistence

**Raster and tile layers**

- `addTileLayer(name, url, options?)` — XYZ tile sources
- `addWmtsLayer(name, url, options?)` — WMTS protocol
- `addWmsLayer(name, options)` — WMS with layer/style configuration

**Cloud-Optimized GeoTIFF (COG)**

```typescript
const layerId = await app.addCogLayer?.(
  "Elevation",
  "https://example.com/dem.tif",
  {
    colormap: "terrain",
    opacity: 0.9,
    nodata: -9999
  }
);

```

Returns `Promise<string>` resolving to the layer ID. Supports band selection, custom colormaps, and nodata handling per `docs/plugin-api.md#app-addcoglayer`.

**Zarr data cubes**

- `addZarrLayer(name, url, options)` — Returns `Promise<string>`; renders v2/v3 Zarr stores
- `setZarrLayerSelector(layerId, selector)` — Update non-spatial dimensions (time, altitude)
- `queryZarrLayer(layerId, geometry, selector?, options?)` — Point/polygon value extraction for identify operations

### Custom WebGL Layer Integration

`registerExternalNativeLayer(registration)` and `unregisterExternalNativeLayer(id)` bridge layers added directly through MapLibre's `CustomLayerInterface` into GeoLibre's state management. This enables:

- Layer appearance in the built-in Layers panel
- Project file persistence
- Optional `paintBridge` for opacity/visibility synchronization

### UI Surface Registration

**Right-sidebar panels** (dockable, resizable)

```typescript
const unregister = app.registerRightPanel?.({
  id: "my-panel",
  title: "Analysis Results",
  defaultWidth: 350,
  render(container: HTMLElement) {
    // Build DOM directly—no React dependency required
    const root = document.createElement("div");
    root.textContent = "Ready";
    container.appendChild(root);
    // Return cleanup function
    return () => root.remove();
  }
});

app.openRightPanel?.("my-panel");

```

Control methods: `openRightPanel(id)`, `closeRightPanel(id)`, `collapseRightPanel(id)`.

**Floating panels** (draggable overlays)

- `registerFloatingPanel(panel)` / `unregisterFloatingPanel(id)`
- `openFloatingPanel(id)`, `closeFloatingPanel(id)`, `getOpenFloatingPanels()`

**Toolbar menus**

`registerToolbarMenu(menu)` adds top-level menus to the GeoLibre banner with full submenu support. Menus support actions, separators, and nested hierarchies.

### Temporal and Animation Support

`registerTemporalLayer(layerId, adapter, { bind? })` links any layer to the global Time Slider. The adapter translates temporal state (current frame, playback direction) into layer-appropriate updates. Essential for animated rasters, time-series visualization, or custom frame-based layers.

### Cross-Plugin Coordination

Plugins can programmatically manage other plugins:

```typescript
// Activate with optional state injection
await app.activatePlugin?.("chart-plugin", {
  dataSource: "temperature-series",
  style: "line"
});

// Graceful shutdown
await app.deactivatePlugin?.("chart-plugin");

```

Use `getProjectState()` and `applyProjectState(state)` to serialize plugin settings into `.geolibre` project files and restore them on load.

### Deck.gl Integration

`getDeckGL()` returns a promise resolving to the host's shared Deck.gl modules:

```typescript
const { core, layers, geoLayers, meshLayers, mapbox } = await app.getDeckGL?.();

```

Rendering on this single instance prevents version conflicts and ensures proper z-ordering with MapLibre layers.

### Utility Methods

| Method | Use case |
|--------|----------|
| `fetchArrayBuffer(url)` | Binary asset loading (COG headers, etc.) |
| `fitBounds(bounds)` | Zoom to `[west, south, east, north]` extent |
| `getMap()` | Access underlying MapLibre `Map` instance |

## Plugin Architecture and Runtime Flow

Understanding how the host loads and manages plugins ensures predictable behavior across environments.

### Registration and Discovery

The `PluginManager` in [`packages/plugins/src/plugin-manager.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/plugin-manager.ts) maintains a registry of `PluginRegistryEntry` objects ([`apps/geolibre-desktop/src/lib/plugin-registry.ts`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/lib/plugin-registry.ts)). At startup:

1. Built-in plugins load from [`packages/plugins/src/index.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/index.ts)
2. Bundled drop-ins load from `public/plugins/`
3. External plugins discovered in user directories load via [`usePlugins.ts`](https://github.com/opengeos/GeoLibre/blob/main/usePlugins.ts)

### Activation Lifecycle

```

Host initializes → PluginManager.register(plugin) → User enables plugin
                                          ↓
                              plugin.activate(app) with GeoLibreAppAPI
                                          ↓
                              Plugin calls API methods, registers UI
                                          ↓
                              User disables plugin → plugin.deactivate(app)

```

State changes propagate automatically to the Zustand-based global store, synchronizing the Layers panel, style editor, and time slider.

### External Plugin Packaging

External plugins distribute as:

- **Zip archives**: [`plugin.json`](https://github.com/opengeos/GeoLibre/blob/main/plugin.json) manifest + compiled `dist/*.js`
- **Directory bundles**: Same structure, loaded directly from filesystem

Integrity verification runs through [`plugin-integrity.ts`](https://github.com/opengeos/GeoLibre/blob/main/plugin-integrity.ts) (hashing, manifest validation) with trust decisions handled by [`plugin-trust.ts`](https://github.com/opengeos/GeoLibre/blob/main/plugin-trust.ts).

## Complete Plugin Examples

### Example 1: Basemap Switcher

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

export const basemapSwitcher: GeoLibrePlugin = {
  id: "basemap-switcher",
  name: "Basemap Switcher",
  version: "0.1.0",
  
  activate(app: GeoLibreAppAPI) {
    app.setBasemap?.(
      "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json"
    );
  },
  
  deactivate() {
    // Intentionally minimal—preserve user's chosen style
  }
};

```

From `docs/plugin-api.md#geolibreplugin`.

### Example 2: COG Viewer with Map Control

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

let navControl: maplibregl.NavigationControl | null = null;

export const cogViewer: GeoLibrePlugin = {
  id: "cog-viewer",
  name: "COG Viewer",
  version: "0.2.0",
  
  async activate(app: GeoLibreAppAPI) {
    const layerId = await app.addCogLayer?.(
      "Satellite DEM",
      "https://example.com/dem.tif",
      { colormap: "terrain", opacity: 0.8 }
    );
    
    const maplibre = await import("maplibre-gl");
    navControl = new maplibre.NavigationControl();
    app.addMapControl?.(navControl, "top-left");
  },
  
  deactivate(app: GeoLibreAppAPI) {
    if (navControl) {
      app.removeMapControl?.(navControl);
      navControl = null;
    }
  }
};

```

From `docs/plugin-api.md#app-addcoglayer`.

### Example 3: Custom Sidebar Panel

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

export const workbench: GeoLibrePlugin = {
  id: "workbench",
  name: "Workbench",
  version: "1.0.0",
  private _cleanup?: () => void,
  
  activate(app: GeoLibreAppAPI) {
    this._cleanup = app.registerRightPanel?.({
      id: "workbench",
      title: "Workbench",
      defaultWidth: 300,
      render(container) {
        const btn = document.createElement("button");
        btn.textContent = "Run analysis";
        btn.onclick = () => console.log("Analysis executed");
        container.appendChild(btn);
        return () => btn.remove();
      }
    });
    
    app.openRightPanel?.("workbench");
  },
  
  deactivate(app: GeoLibreAppAPI) {
    app.closeRightPanel?.("workbench");
    this._cleanup?.();
  }
};

```

From `docs/plugin-api.md#app-registerrightpanel`.

## Key Source Files for Plugin Development

| File | Purpose |
|------|---------|
| [`docs/plugin-api.md`](https://github.com/opengeos/GeoLibre/blob/main/docs/plugin-api.md) | Canonical API specification—all interfaces and types |
| [`packages/plugins/src/plugin-manager.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/plugin-manager.ts) | `PluginManager` implementation |
| [`apps/geolibre-desktop/src/lib/plugin-registry.ts`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/lib/plugin-registry.ts) | `PluginRegistryEntry` definition |
| [`apps/geolibre-desktop/src/lib/plugin-integrity.ts`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/lib/plugin-integrity.ts) | External plugin validation |
| [`apps/geolibre-desktop/src/lib/plugin-trust.ts`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/lib/plugin-trust.ts) | Sandboxing and permissions |
| [`apps/geolibre-desktop/src/hooks/usePlugins.ts`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/hooks/usePlugins.ts) | Startup loading logic |
| [`tests/plugin-manager.test.ts`](https://github.com/opengeos/GeoLibre/blob/main/tests/plugin-manager.test.ts) | Lifecycle behavior verification |

## Summary

- **The `GeoLibrePlugin` interface** defines your plugin's identity and lifecycle hooks—implement `activate()` and `deactivate()` as the foundation
- **The `GeoLibreAppAPI` object** exposes 30+ methods across map control, layer management, UI registration, and plugin coordination
- **All API methods are optional**—use optional chaining (`app.method?.()`) for defensive coding across host variants
- **Cloud-native formats** (COG, Zarr) have dedicated methods with Promise-based async signatures
- **UI registration uses plain DOM**—no React dependency required, just return cleanup functions from `render()`
- **Cross-plugin activation and project state persistence** enable composable, multi-plugin workflows

## Frequently Asked Questions

### What is the minimum viable GeoLibre plugin?

A valid plugin requires only `id`, `name`, `version`, and an `activate` method. The `deactivate` hook is optional but strongly recommended for resource cleanup. All other capabilities—URL handling, state persistence, UI registration—are opt-in through additional properties and method calls.

### Can plugins access the raw MapLibre GL map instance?

Yes. Call `app.getMap()` to retrieve the underlying `Map` instance, or `null` if the map isn't ready. Use this for advanced operations not covered by the high-level API, but note that direct map manipulation bypasses GeoLibre's state synchronization—consider `registerExternalNativeLayer` for custom layers that need UI integration.

### How do plugins handle project file persistence?

Call `app.getProjectState()` during `deactivate()` to capture your plugin's settings, then return them in the state object. On project load, GeoLibre calls `activate()` and your plugin can read the restored state from `app.getProjectState()`. The `.geolibre` file format stores plugin state under each plugin's `id` key.