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

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.

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.

// 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):

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, 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:

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 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 Defines GeoLibreExternalNativeLayerRegistration, ExternalNativePaintBridge, and paintMode discriminator
/packages/plugins/src/plugin-manager.ts Manages plugin lifecycle, activation generations, and register/unregister orchestration
/packages/map/src/layer-sync.ts Observes store state and creates MapLibre CustomLayerInterface instances for WebGL layers
/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, 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.

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 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.

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 →