# How Plugins Register Custom WebGL Layers and Ensure Persistence in GeoLibre Projects

> Learn how GeoLibre plugins register custom WebGL layers and ensure they persist across sessions using registerExternalNativeLayer and re-establishing the paintBridge.

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

---

**GeoLibre plugins register custom WebGL layers by calling `registerExternalNativeLayer` with a declarative `GeoLibreExternalNativeLayerRegistration` object marked with `paintMode: "plugin"`, then re-establish the runtime `paintBridge` after project restore to ensure full functionality across sessions.**

The **opengeos/GeoLibre** framework provides a dedicated API for plugins to inject **MapLibre `CustomLayerInterface`** layers—pure WebGL rendering pipelines that bypass MapLibre's standard paint system. This architecture separates declarative layer state (persisted with projects) from runtime rendering logic (re-registered by plugins), enabling robust plugin-managed visualization layers that survive project reloads.

## The Registration Flow for Custom WebGL Layers

Plugins gain access to the host application's API through the `GeoLibreAppAPI` interface. The `registerExternalNativeLayer` method is the entry point for all external native layer registrations in [`/packages/plugins/src/types.ts`](https://github.com/opengeos/GeoLibre/blob/main//packages/plugins/src/types.ts).

### Core Registration Object

A plugin constructs a `GeoLibreExternalNativeLayerRegistration` containing:

- **`id`** and **`name`** — unique identifier and display label
- **`type`** — any valid MapLibre layer type (e.g., `"raster"`, `"fill"`)
- **`paintMode: "plugin"`** — the critical flag that signals WebGL custom rendering
- **`paintBridge`** — optional setter object linking UI controls to plugin renderers

The `paintMode: "plugin"` value in `/packages/plugins/src/types.ts#L53-L60` instructs GeoLibre's core to treat this layer as a **custom WebGL layer** rather than routing it through standard MapLibre paint properties.

```typescript
// Inside a plugin's activate() method
export const myWebGLLayerPlugin: GeoLibrePlugin = {
  id: "my-webgl-layer",
  name: "My WebGL Layer",
  activate: (app) => {
    const paintBridge: ExternalNativePaintBridge = {
      setOpacity: (opacity) => myRenderer.setOpacity(opacity),
    };

    app.registerExternalNativeLayer?.({
      id: "my-layer",
      name: "My Layer",
      type: "raster",
      nativeLayerIds: [],
      paintMode: "plugin",      // <-- Enables WebGL custom rendering
      paintBridge,              // <-- UI controls bind here
      opacity: 1,
    });
  },

  deactivate: (app) => {
    app.unregisterExternalNativeLayer?.("my-layer");
  },
};

```

The `paintBridge` object at `/packages/plugins/src/types.ts#L63-L70` exposes typed setter functions (`setOpacity`, `setVisibility`, etc.) that GeoLibre's generic layer UI invokes. These bridge calls forward to the plugin's internal WebGL renderer state.

## Lifecycle Management and Cleanup

The `PluginManager` in `/packages/plugins/src/plugin-manager.ts#L74-L98` tracks active plugin generations. When a plugin deactivates—whether through user action, dependency failure, or explicit unload—it must call `unregisterExternalNativeLayer(id)`:

```typescript
deactivate: (app) => {
  app.unregisterExternalNativeLayer?.("my-layer");
}

```

This triggers:

1. Removal of the layer record from GeoLibre's internal Zustand store
2. Cleanup of any pending activation generation in the `PluginManager`
3. Automatic detachment from the MapLibre map instance via `MapController`

## Project Persistence: The Serialization Boundary

The **critical architectural constraint** in GeoLibre's persistence model is the serialization boundary between declarative state and runtime functions.

### What Gets Persisted

The `GeoLibreExternalNativeLayerRegistration` is **fully serializable**—all fields are plain JSON-serializable data. When a project saves to [`.geolibre.json`](https://github.com/opengeos/GeoLibre/blob/main/.geolibre.json), the entire registration object is written including:

- `id`, `name`, `type`, `nativeLayerIds`
- `paintMode: "plugin"`
- Numeric properties (`opacity`, `visibility`, etc.)

### What Gets Lost

The `paintBridge` contains **function references** that cannot be serialized. As documented in `/packages/plugins/src/types.ts#L70-L73`, the bridge is explicitly omitted from project files.

### Restoration Requirements

On project load, GeoLibre reconstructs the layer record from JSON, but the `paintBridge` is absent. The plugin **must re-register the layer** to restore full functionality:

```typescript
activate: (app) => {
  // Layer record exists from project file, but paintBridge is missing
  const paintBridge = {
    setOpacity: (opacity) => myRenderer.setOpacity(opacity),
  };

  // Re-register to re-establish the bridge
  app.registerExternalNativeLayer?.({
    id: "my-layer",
    name: "My Layer",
    type: "raster",
    nativeLayerIds: [],
    paintMode: "plugin",
    paintBridge,
    opacity: 1,
  });
}

```

Plugins typically handle this in one of two patterns:

- **Direct re-registration in `activate`** — always register, letting GeoLibre deduplicate by `id`
- **Post-restore listener** — respond to `restoreProjectState` completion, then register if the layer record exists but lacks a bridge

## Map Synchronization Architecture

The `MapController` in [`/packages/map/src/layer-sync.ts`](https://github.com/opengeos/GeoLibre/blob/main//packages/map/src/layer-sync.ts) implements reactive layer synchronization. It observes the Zustand store for `GeoLibreExternalNativeLayerRegistration` entries and performs:

1. **Detection** — identify entries with `paintMode: "plugin"`
2. **Stub creation** — instantiate a MapLibre `CustomLayerInterface` proxy
3. **Rendering delegation** — forward all `render()` and `prerender()` calls to the plugin's registered drawing functions

Because layer records live in the central store rather than being owned by any single component, they survive:

- Hot module replacement during development
- Full UI reloads (e.g., settings panel toggles)
- Map instance destruction and recreation

The store-driven architecture ensures that **custom WebGL layers remain synchronized with project state** regardless of UI lifecycle events.

## Key Source Files and Responsibilities

| File | Responsibility |
|------|--------------|
| [`/packages/plugins/src/types.ts`](https://github.com/opengeos/GeoLibre/blob/main//packages/plugins/src/types.ts) | Defines `GeoLibreExternalNativeLayerRegistration`, `ExternalNativePaintBridge`, and `paintMode` discriminator |
| [`/packages/plugins/src/plugin-manager.ts`](https://github.com/opengeos/GeoLibre/blob/main//packages/plugins/src/plugin-manager.ts) | Manages plugin lifecycle, activation generations, and `register`/`unregister` orchestration |
| [`/packages/map/src/layer-sync.ts`](https://github.com/opengeos/GeoLibre/blob/main//packages/map/src/layer-sync.ts) | Observes store state and creates MapLibre `CustomLayerInterface` instances for WebGL layers |
| [`/packages/core/src/external-native-paint.ts`](https://github.com/opengeos/GeoLibre/blob/main//packages/core/src/external-native-paint.ts) | Core type definitions (`ExternalNativePaintMode`, `ExternalNativePaintBridge`) and paint utilities |

## Summary

- **Registration** — Plugins call `app.registerExternalNativeLayer()` with `paintMode: "plugin"` to enable WebGL custom rendering
- **Bridge pattern** — The `paintBridge` object decouples GeoLibre's generic UI controls from plugin-specific renderer implementations
- **Serialization limits** — Layer metadata persists to [`.geolibre.json`](https://github.com/opengeos/GeoLibre/blob/main/.geolibre.json), but function-based bridges do not
- **Restoration requirement** — Plugins must re-register layers after project load to re-establish the `paintBridge`
- **Reactive sync** — The `MapController` automatically synchronizes store-based layer records with the MapLibre map instance

## Frequently Asked Questions

### What happens if a plugin doesn't re-register its WebGL layer after project restore?

The layer record persists and displays in the layer panel, but all interactive controls (opacity, visibility toggles) become non-functional. The layer may render with default state or fail to render entirely, depending on whether the plugin's renderer independently manages its lifecycle. Per `/packages/plugins/src/types.ts#L70-L73`, the bridge must be re-registered to restore full functionality.

### Can multiple plugins register layers with the same `id`?

No. The `PluginManager` enforces unique layer identifiers across all registrations. Attempting to register a duplicate `id` typically results in the second registration being ignored or throwing a runtime error, depending on the activation generation state tracked in [`/packages/plugins/src/plugin-manager.ts`](https://github.com/opengeos/GeoLibre/blob/main//packages/plugins/src/plugin-manager.ts).

### Does `paintMode: "plugin"` support all MapLibre layer types?

The `type` field accepts any MapLibre layer type string, but when `paintMode: "plugin"` is set, the standard paint pipeline is bypassed entirely. The plugin receives full control via the `CustomLayerInterface` methods regardless of the declared type. The `type` primarily affects how GeoLibre categorizes the layer in its internal UI components.

### How does GeoLibre handle WebGL context loss for plugin layers?

The `MapController` in [`/packages/map/src/layer-sync.ts`](https://github.com/opengeos/GeoLibre/blob/main//packages/map/src/layer-sync.ts) manages the `CustomLayerInterface` stub, which includes standard MapLibre lifecycle hooks. Plugins should implement `onAdd` and `onRemove` on their renderers to handle context recreation. The reactive store ensures layer records survive context loss, and the bridge re-establishes automatically when the plugin re-registers after any map reinitialization.