# How to Implement Custom Layer Types in the GeoLibre Plugin System

> Implement custom layer types in GeoLibre using the plugin system. Register your MapLibre custom layer in activate and clean up in deactivate for full functionality.

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

---

**Create a MapLibre custom layer inside your plugin's `activate` method, register it with `app.registerExternalNativeLayer()`, and clean up on `deactivate` to add fully functional custom layers that appear in GeoLibre's Layers panel.**

GeoLibre's plugin architecture lets you extend the core map engine with bespoke layer types that behave exactly like native layers. Whether you need a custom WebGL renderer, a novel data format, or specialized visualization, you can implement custom layer types in the GeoLibre plugin system while maintaining full UI integration—including opacity sliders, visibility toggles, and project persistence. This guide walks through the complete implementation based on the opengeos/GeoLibre source code.

## Prerequisites and Core Types

Before building a custom layer, understand the foundational types defined in [`packages/plugins/src/types.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/types.ts). These interfaces govern how plugins interact with GeoLibre's application API.

The two critical types for custom layers are:

- **`GeoLibrePlugin`**: The interface your plugin must implement, with `activate()` and `deactivate()` lifecycle methods
- **`GeoLibreExternalNativeLayerRegistration`**: The configuration object that bridges your MapLibre layer into GeoLibre's state management

## Step-by-Step Implementation

### 1. Define Your Custom Layer Class

Create a class that implements MapLibre's `CustomLayerInterface`. This is your actual rendering layer—typically WebGL-based.

```typescript
// src/plugins/my-custom-layer.ts
import type {
  GeoLibreAppAPI,
  GeoLibrePlugin,
  GeoLibreExternalNativeLayerRegistration,
} from "@geolibre/plugins";

class MyWebGLLayer implements maplibregl.CustomLayerInterface {
  id = "my-webgl-layer";
  
  constructor(private gl: WebGLRenderingContext) {}

  onAdd(map: maplibregl.Map) {
    // Allocate WebGL buffers, compile shaders, initialize state
  }

  render(gl: WebGLRenderingContext, matrix: Float32Array) {
    // Execute draw calls for your custom visualization
  }

  // Custom methods for UI integration
  setOpacity(opacity: number) {
    // Update shader uniform for alpha blending
  }

  setActive(active: boolean) {
    // Toggle visibility in render loop
  }
}

```

The `id` property must be unique across all layers in the map instance.

### 2. Implement the Plugin Interface

Structure your plugin according to `GeoLibrePlugin` from [`packages/plugins/src/types.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/types.ts). The `activate` method is where you wire everything together.

```typescript
export const myCustomLayerPlugin: GeoLibrePlugin = {
  id: "my-custom-layer",
  name: "My Custom Layer",
  version: "0.1.0",

  activate(app: GeoLibreAppAPI) {
    // Obtain WebGL context from MapLibre's canvas
    const gl = (app.getMap?.()?.getCanvas()?.getContext("webgl") ??
      undefined) as WebGLRenderingContext;
    
    const layer = new MyWebGLLayer(gl);

    // Add to MapLibre (does NOT automatically appear in GeoLibre UI)
    app.getMap?.()?.addLayer({
      id: layer.id,
      type: "custom",
      renderingMode: "3d",
      customLayer: layer,
    });

    // Register with GeoLibre to enable UI integration
    const registration: GeoLibreExternalNativeLayerRegistration = {
      id: layer.id,
      name: "My WebGL Layer",
      type: "raster",           // Closest built-in type for icon/styling
      nativeLayerIds: [layer.id],
      paintMode: "plugin",      // Critical: tells GeoLibre you handle paint
      paintBridge: {
        setOpacity: (opacity) => layer.setOpacity(opacity),
        setVisibility: (visible) => layer.setActive(visible),
      },
    };
    
    app.registerExternalNativeLayer?.(registration);
  },

  deactivate(app: GeoLibreAppAPI) {
    // Clean up MapLibre layer
    app.getMap?.()?.removeLayer("my-webgl-layer");
    // Remove from GeoLibre's store
    app.unregisterExternalNativeLayer?.("my-webgl-layer");
  },
};

```

The `paintMode: "plugin"` setting is essential when implementing custom layer types in the GeoLibre plugin system with their own rendering logic. Without this, GeoLibre would expose generic raster controls that have no effect on your layer.

### 3. Expose and Register the Plugin

Export your plugin from the package entry point at [`packages/plugins/src/index.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/index.ts):

```typescript
export { myCustomLayerPlugin } from "./plugins/my-custom-layer";

```

Then register it in the application bootstrap at [`apps/geolibre-desktop/src/hooks/usePlugins.ts`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/hooks/usePlugins.ts):

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

manager.registerAll([
  // ...other built-in plugins
  myCustomLayerPlugin,
]);

```

## Understanding the Registration Configuration

The `GeoLibreExternalNativeLayerRegistration` object controls how GeoLibre represents your layer in the UI and state system:

| Field | Purpose | Required |
|-------|---------|----------|
| `id` | Unique identifier matching the MapLibre layer ID | Yes |
| `name` | Display name in the Layers panel | Yes |
| `type` | Native type hint (`"raster"`, `"vector"`, `"fill"`, etc.) for icons | Yes |
| `nativeLayerIds` | Array of MapLibre layer IDs belonging to this logical layer | Yes |
| `paintMode` | `"plugin"` or `"native"`—whether paint properties are handled externally | Yes |
| `paintBridge` | Callbacks for `setOpacity` and `setVisibility` when `paintMode: "plugin"` | Recommended |

## paintBridge Callbacks Explained

When `paintMode` is `"plugin"`, GeoLibre cannot directly manipulate your layer's visual properties because it doesn't know your internal rendering state. The `paintBridge` provides the translation layer:

- **`setOpacity(opacity: number)`**: Called when the user adjusts the opacity slider in the Layers panel. Receives a value from 0 to 1.
- **`setVisibility(visible: boolean)`**: Called when the user toggles layer visibility via checkbox or eye icon.

Without these callbacks, your layer renders but the UI controls remain disconnected.

## Alternative: Native Paint Mode

If your custom layer uses standard MapLibre paint properties, you can set `paintMode: "native"` and omit `paintBridge`. GeoLibre will manipulate `raster-opacity` or equivalent properties directly. This only works for layers that don't implement `CustomLayerInterface`.

## Lifecycle and Cleanup

Proper cleanup in `deactivate` prevents memory leaks and dangling references:

1. Remove the MapLibre layer with `app.getMap?.()?.removeLayer(id)`
2. Unregister from GeoLibre's store with `app.unregisterExternalNativeLayer?.(id)`

The order matters: unregister from GeoLibre first if you need to access the map instance during cleanup, though typically map removal comes first.

## Key Source Files Reference

| File | Location | Purpose |
|------|----------|---------|
| Plugin type definitions | [`packages/plugins/src/types.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/types.ts) | `GeoLibrePlugin`, `GeoLibreAppAPI`, `GeoLibreExternalNativeLayerRegistration` |
| Plugin manager | [`packages/plugins/src/plugin-manager.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/plugin-manager.ts) | Lifecycle orchestration and activation |
| Package exports | [`packages/plugins/src/index.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/index.ts) | Public API surface for built-in plugins |
| Application integration | [`apps/geolibre-desktop/src/hooks/usePlugins.ts`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/hooks/usePlugins.ts) | Runtime plugin registration |
| API documentation | [`docs/plugin-api.md`](https://github.com/opengeos/GeoLibre/blob/main/docs/plugin-api.md) | Complete reference including Custom (WebGL) layers section |
| User guide | [`docs/user-guide/plugins.md`](https://github.com/opengeos/GeoLibre/blob/main/docs/user-guide/plugins.md) | End-user documentation for plugin management |

## Summary

- **Create** a `CustomLayerInterface` implementation for WebGL rendering or use standard MapLibre layer types
- **Register** with `app.registerExternalNativeLayer()` using `paintMode: "plugin"` and `paintBridge` callbacks for full UI control
- **Clean up** both MapLibre and GeoLibre registrations in `deactivate` to prevent leaks
- **Export** from [`packages/plugins/src/index.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/index.ts) and register in [`usePlugins.ts`](https://github.com/opengeos/GeoLibre/blob/main/usePlugins.ts) for activation

## Frequently Asked Questions

### What is the difference between `addLayer()` and `registerExternalNativeLayer()` in GeoLibre?

`app.getMap?.()?.addLayer()` adds your layer to the underlying MapLibre map instance, making it render on the canvas. However, GeoLibre's UI and state system remain unaware of it. `app.registerExternalNativeLayer()` mirrors the layer into GeoLibre's store, causing it to appear in the Layers panel, participate in opacity/visibility controls, and persist in project files. Both calls are required for full integration.

### Can I implement custom layer types without WebGL?

Yes. Any valid MapLibre layer type—including `fill`, `line`, `circle`, or `raster` with custom sources—can be registered as an external native layer. Only layers that implement `CustomLayerInterface` require `paintMode: "plugin"`. Standard layers can use `paintMode: "native"` and let GeoLibre manipulate paint properties directly.

### How do multiple `nativeLayerIds` work in a single registration?

Some complex visualizations require multiple MapLibre layers working together—perhaps a base fill layer with an outline stroke. Pass all related layer IDs in `nativeLayerIds: ["fill-layer", "outline-layer"]`. GeoLibre treats them as a single logical layer in the UI, so one visibility toggle controls both, and opacity changes apply to the primary layer while the secondary follows.

### What happens if I forget to call `unregisterExternalNativeLayer()` in `deactivate`?

The layer remains in GeoLibre's internal store as a stale reference. This causes several problems: the Layers panel displays a non-functional entry, project serialization may include invalid layer state, and re-activating the plugin can trigger ID collision errors. Always pair registration with unregistration in the plugin lifecycle.