# How the GeoLibre Plugin System Manages Plugins: PluginManager and FloatingPanelRegistry Explained

> Understand how GeoLibre's plugin system uses PluginManager for registration and activation and FloatingPanelRegistry for UI overlays. Explore the opengeos/GeoLibre repository.

- Repository: [Open Geospatial Solutions/GeoLibre](https://github.com/opengeos/GeoLibre)
- Tags: internals
- Published: 2026-08-04

---

**The GeoLibre plugin system orchestrates independently-lifecycled functionality through two core modules: `PluginManager` handles registration, activation, and state persistence, while `FloatingPanelRegistry` manages reactive UI overlays that plugins can render.**

GeoLibre treats plugins as first-class citizens with isolated lifecycles and scoped APIs. According to the opengeos/GeoLibre source code, the architecture separates business logic orchestration from UI presentation, enabling developers to build map tools that integrate seamlessly with the desktop application’s React-based interface.

## Core Architecture: PluginManager and FloatingPanelRegistry

The plugin architecture centers on two distinct registries that communicate through well-defined APIs.

**`PluginManager`** ([`packages/plugins/src/plugin-manager.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/plugin-manager.ts)) serves as the central authority for plugin lifecycles. It maintains a map of registered plugins, tracks active states, handles asynchronous activation with rollback protection, processes deep-link URL parameters, and persists project-specific settings.

**`FloatingPanelRegistry`** ([`packages/plugins/src/floating-panel-registry.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/floating-panel-registry.ts)) provides an imperative API for draggable UI panels. It exposes open/close/focus operations and supplies a reactive snapshot compatible with React’s `useSyncExternalStore`, ensuring UI components re-render only when panel visibility or stacking order changes.

## PluginManager: Lifecycle and State Management

### Plugin Registration and Default Activation

Plugins enter the system through the `register` method in [`packages/plugins/src/plugin-manager.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/plugin-manager.ts). This method validates the plugin, stores default map control positions, and immediately activates plugins marked with `activeByDefault`.

```typescript
// packages/plugins/src/plugin-manager.ts (lines 24-48)
register(plugin: GeoLibrePlugin): void {
  // Replace previous instance if hot-reloaded
  this.plugins.set(plugin.id, plugin);
  this.urlParameterNamesById.set(plugin.id,
    normalizeUrlParameterNames(plugin.urlParameterNames));
  
  const defaultPos = plugin.getMapControlPosition?.();
  if (defaultPos) this.defaultMapControlPositions.set(plugin.id, defaultPos);
  
  // Mark active if the plugin declares activeByDefault
  if (plugin.activeByDefault) {
    this.defaultActive.add(plugin.id);
    this.active.add(plugin.id);
  }
  this.notify();
}

```

The registration process captures URL parameter names for later deep-linking and notifies all subscribers of the state change.

### Activation Flow and Async Mounting

The `activate` method (lines 62-84) handles both synchronous and asynchronous plugin initialization. It scopes the application API to prevent privilege escalation, tracks activation state to prevent concurrent activation attempts, and watches returned Promises to handle mount failures gracefully.

```typescript
activate(id: string, app: GeoLibreAppAPI): boolean | Promise<boolean> {
  const plugin = this.plugins.get(id);
  if (!plugin || this.activating.has(id)) return false;
  if (this.active.has(id)) return true;          // already active
  
  const scopedApp = scopeAppToPlugin(app, id);
  this.activating.add(id);
  let result: ReturnType<GeoLibrePlugin['activate']>;
  
  try { result = plugin.activate(scopedApp); }
  finally { this.activating.delete(id); }

  if (result === false) return false;            // refused activation
  
  const gen = this.nextActivationGeneration(id);
  this.active.add(id);
  this.notify();

  // Watch Promise results to roll back on failure
  const asyncResult = this.watchAsyncActivation(id, result, scopedApp, gen);
  if (asyncResult) this.trackActivationResult(id, asyncResult);
  return asyncResult ?? true;
}

```

The `watchAsyncActivation` helper (lines 98-118) protects against stale callbacks using generation counters. If an async activation fails, it automatically removes the plugin from the active set and cleans up resources.

### Scoped Application API

When a plugin receives the application API, GeoLibre scopes it via `scopeAppToPlugin` (lines 443-489). This wrapper automatically tags UI registrations with the plugin’s ID and prevents plugins from deactivating themselves, avoiding lifecycle loops.

```typescript
function scopeAppToPlugin(app, pluginId, options = {}): GeoLibreAppAPI {
  const scoped = { ...app };
  
  // Tag toolbar menus with owner id
  if (app.registerToolbarMenu) {
    scoped.registerToolbarMenu = (menu) =>
      (app.registerToolbarMenu as any)(menu, pluginId);
  }
  
  // Guard against self-deactivation loops
  if (app.deactivatePlugin) {
    scoped.deactivatePlugin = (targetId) =>
      targetId === pluginId ? false : app.deactivatePlugin(targetId);
  }
  
  return scoped;
}

```

### Deep-Linking with URL Parameters

The `handleUrlParameters` method (lines 323-368) enables deep-linking by dispatching URL query parameters to interested plugins. It deduplicates work per context using `handledUrlParametersByContext` and `inFlightUrlContexts`, ensuring handlers execute exactly once even during overlapping async dispatches.

```typescript
for (const [id, plugin] of this.plugins) {
  if (!plugin.handleUrlParameters) continue;
  const names = this.urlParameterNamesById.get(id) ?? [];
  if (!names.some(n => params.has(n))) continue;  // plugin not involved

  if (!this.active.has(id)) {
    const activated = await this.activate(id, app);
    if (!activated) continue;
  }
  await plugin.handleUrlParameters(scopeAppToPlugin(app, id), params);
}

```

### Project State Persistence

GeoLibre captures complete plugin configurations via `getProjectState()` and restores them through `restoreProjectState()` (lines 549-595). This system records map control positions, custom plugin settings, and activation states, then reconstructs the exact UI layout when users reopen saved projects.

The restoration process re-applies saved positions, injects stored settings via `applyProjectState`, and reactivates plugins while watching for async failures using the same `watchAsyncActivation` mechanism used during normal activation.

## FloatingPanelRegistry: UI Management for Plugins

While `PluginManager` handles logic lifecycle, `FloatingPanelRegistry` ([`packages/plugins/src/floating-panel-registry.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/floating-panel-registry.ts)) manages presentation layer concerns for draggable, map-overlay panels.

### Registering Floating Panels

Plugins register panels during their activation hook using `registerFloatingPanel` (lines 60-82). The registry validates required fields, normalizes title strings vs. functions, and returns an unregister callback for cleanup during deactivation.

```typescript
export function registerFloatingPanel(panel: GeoLibreFloatingPanelRegistration): () => void {
  if (!panel.id) throw new Error("Panel id is required");
  if (typeof panel.title !== "string" && typeof panel.title !== "function")
    throw new Error(`Floating panel "${panel.id}" must have a title`);
    
  titleResolver.set(panel);
  registry.set(panel.id, panel);
  emit();  // Bump version & notify subscribers
  
  return () => {
    if (registry.get(panel.id) === panel) unregisterFloatingPanel(panel.id);
  };
}

```

### Controlling Panel Visibility

The registry exposes imperative methods to manipulate the panel stack. `openFloatingPanel` (lines 102-115) brings panels to the front of the z-order and triggers `onOpen` callbacks, while `closeFloatingPanel` removes IDs from the open set and executes `onClose` hooks.

```typescript
export function openFloatingPanel(id: string): boolean {
  const panel = registry.get(id);
  if (!panel) { console.warn("Panel not found:", id); return false; }
  
  const wasOpen = openIds.includes(id);
  openIds = [...openIds.filter(o => o !== id), id]; // bring to front
  emit();
  
  if (!wasOpen) runHook(id, "onOpen", panel.onOpen);
  return true;
}

```

Focus operations simply reorder the `openIds` array without triggering open/close lifecycle hooks, allowing users to bring background panels forward without full remounts.

### Reactive Snapshots for React Integration

The registry implements the `useSyncExternalStore` pattern via `getFloatingPanelsSnapshot` and `subscribeFloatingPanels` (lines 167-178). This ensures React components re-render only when the set or order of open panels changes, not on every internal registry mutation.

```typescript
export function getFloatingPanelsSnapshot(): FloatingPanelsSnapshot {
  return snapshot;   // { openIds, version }
}

export function subscribeFloatingPanels(listener: () => void): () => void {
  listeners.add(listener);
  return () => listeners.delete(listener);
}

```

## Practical Implementation Examples

### Registering a Basic Plugin

Define a plugin that registers a toolbar menu and activates by default:

```typescript
// my-plugin.ts
import type { GeoLibrePlugin } from "@geolibre/plugins";

export const myPlugin: GeoLibrePlugin = {
  id: "my-plugin",
  title: "Demo Plugin",
  activeByDefault: true,
  
  activate(app) {
    console.log("My plugin activated");
    const unregister = app.registerToolbarMenu({
      label: "Demo",
      command: () => alert("Demo command"),
    });
    return () => unregister();  // Cleanup on deactivation
  },
  
  deactivate(app) {
    console.log("My plugin deactivated");
  },
};

```

Register the plugin at application startup:

```typescript
// packages/plugins/src/builtins.ts
import { PluginManager } from "./plugin-manager";
import { myPlugin } from "./my-plugin";

PluginManager.register(myPlugin);

```

### Creating a Floating Panel Plugin

Expose draggable UI by registering a panel during activation:

```typescript
import { registerFloatingPanel, openFloatingPanel } from "@geolibre/plugins";

export const myPlugin = {
  id: "floating-demo",
  
  activate(app) {
    const unregister = registerFloatingPanel({
      id: "demo-panel",
      title: () => "Demo Panel",
      render: (container) => {
        container.innerHTML = `<p>Hello from a floating panel!</p>`;
      },
      onOpen: () => console.log("panel opened"),
      onClose: () => console.log("panel closed"),
    });
    
    openFloatingPanel("demo-panel");
    return unregister;  // Auto-close on deactivation
  },
  
  deactivate(app) {
    // Cleanup handled by unregister callback
  },
};

```

### Consuming Panels in React Components

Render open panels using the reactive snapshot:

```typescript
import { useSyncExternalStore } from "react";
import {
  getFloatingPanelsSnapshot,
  subscribeFloatingPanels,
  getFloatingPanel,
} from "@geolibre/plugins";

function FloatingPanels() {
  const snapshot = useSyncExternalStore(
    subscribeFloatingPanels,
    getFloatingPanelsSnapshot,
  );

  return (
    <>
      {snapshot.openIds.map((id) => {
        const panel = getFloatingPanel(id);
        if (!panel) return null;
        return (
          <FloatingPanelCard key={id} title={panel.title}>
            {panel.render(containerRef.current)}
          </FloatingPanelCard>
        );
      })}
    </>
  );
}

```

## Summary

- **PluginManager** ([`packages/plugins/src/plugin-manager.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/plugin-manager.ts)) serves as the central lifecycle authority, handling registration, activation, deactivation, URL deep-linking, and project state persistence for the GeoLibre plugin system.
- **FloatingPanelRegistry** ([`packages/plugins/src/floating-panel-registry.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/floating-panel-registry.ts)) provides imperative UI management for draggable panels, exposing open/close/focus operations and reactive snapshots via `useSyncExternalStore`.
- The **scoped application API** (`scopeAppToPlugin`) isolates plugins by tagging UI registrations with owner IDs and preventing self-deactivation loops.
- **Async activation protection** uses generation counters and `watchAsyncActivation` to handle Promise-based mounts, automatically rolling back state on failure.
- **Project state persistence** captures map control positions and custom settings, restoring exact plugin configurations across application sessions.

## Frequently Asked Questions

### How does GeoLibre handle plugin activation failures?

The `PluginManager.activate` method in [`packages/plugins/src/plugin-manager.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/plugin-manager.ts) returns `false` for immediate failures and watches Promise-based activations via `watchAsyncActivation` (lines 98-118). If an async mount rejects, the manager automatically removes the plugin ID from the active set and cleans up any partial registrations, ensuring the application remains stable.

### Can plugins activate other plugins?

Yes, but with restrictions. The scoped application API (`scopeAppToPlugin`, lines 443-489) wraps the `activatePlugin` method to prevent a plugin from activating or deactivating itself, which would create lifecycle loops. However, plugins can activate other plugins by ID through the scoped API, and the system tracks concurrent activation attempts via the `activating` Set to prevent race conditions.

### What happens to floating panels when a plugin deactivates?

Floating panels registered via `registerFloatingPanel` return an unregister callback that should be invoked during the plugin's `deactivate` hook. This callback automatically removes the panel from the registry and closes any open instances. If the cleanup is omitted, the panel remains in the registry but becomes non-functional once the plugin's render context disappears.

### How does the floating panel system optimize React rendering?

The `FloatingPanelRegistry` implements the `useSyncExternalStore` pattern (lines 167-178) by exposing `getFloatingPanelsSnapshot` and `subscribeFloatingPanels`. The snapshot contains an `openIds` array and a version counter; React components only re-render when the version changes, which occurs only when panels open, close, or reorder. This prevents unnecessary renders during internal registry mutations.