# How to Load External Plugins from ZIP Files and plugin.json Manifests in GeoLibre

> Discover how GeoLibre loads external plugins from ZIP files or plugin.json manifests using its three-phase pipeline: discovery, validation, and activation.

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

---

**GeoLibre loads external plugins through a three-phase pipeline—discovery, validation, and activation—supporting ZIP archives, unpacked directories, and remote HTTPS URLs pointing to [`plugin.json`](https://github.com/opengeos/GeoLibre/blob/main/plugin.json) manifests.**

GeoLibre's plugin system treats external extensions as **self-contained bundles** that can be distributed as ZIP files, loaded from local directories, or fetched directly from HTTPS endpoints. This article explains the complete loading pipeline implemented in the `opengeos/GeoLibre` desktop client, including the specific files and validation rules that govern how external plugins are discovered and activated.

## Plugin Bundle Formats

GeoLibre accepts external plugins in three distinct formats:

- **ZIP archives** containing a [`plugin.json`](https://github.com/opengeos/GeoLibre/blob/main/plugin.json) manifest at the root
- **Unpacked directories** with the same structure, typically used during plugin development
- **Remote HTTPS URLs** pointing directly to a [`plugin.json`](https://github.com/opengeos/GeoLibre/blob/main/plugin.json) file

Each format follows identical validation rules once the manifest is located.

## Discovery Phase: Locating plugin.json

The discovery process varies by source type, with dedicated handlers for each format.

### ZIP Archive Discovery

When users install a plugin via **Manage Plugins → Settings → Install from file**, the file is passed to [`plugin-archive-unpack.ts`](https://github.com/opengeos/GeoLibre/blob/main/plugin-archive-unpack.ts). The helper function `locatePluginJsonInArchive` scans ZIP entries and returns the path of the first [`plugin.json`](https://github.com/opengeos/GeoLibre/blob/main/plugin.json) found, preferring root manifests and ignoring macOS `__MACOSX/` folders. Per the source code at lines 76-100, the scanner handles nested directories by selecting the shallowest valid manifest.

### Local Directory Discovery

For development copies or manually installed plugins, [`bundled-plugins.ts`](https://github.com/opengeos/GeoLibre/blob/main/bundled-plugins.ts) reads `plugins/<dir>/plugin.json` directly from the filesystem (lines 21-31). This path is used for bundled drop-ins that ship with the application.

### Remote URL Discovery

Remote manifests are stored in the project's `manifestUrls` array (documented in [`project-format.md`](https://github.com/opengeos/GeoLibre/blob/main/project-format.md)). When the app starts or a user adds a URL, `loadExternalPluginFromUrl` fetches the JSON file. The implementation at lines 278-284 of [`external-plugins.ts`](https://github.com/opengeos/GeoLibre/blob/main/external-plugins.ts) handles the network request and initial parsing.

## Validation Phase: Security and Integrity Checks

After discovery, GeoLibre enforces strict validation before accepting any plugin. The following checks are performed in sequence:

| Check | Requirement | Implementation Location |
|-------|-------------|------------------------|
| **JSON syntax** | Valid parseable JSON | [`plugin-archive-unpack.ts`](https://github.com/opengeos/GeoLibre/blob/main/plugin-archive-unpack.ts) lines 122-124 |
| **Required fields** | `id`, `name`, `version`, and `entry` must be present; `manifestUrl` required for remote plugins | [`external-plugins.ts`](https://github.com/opengeos/GeoLibre/blob/main/external-plugins.ts) lines 421-428 |
| **Entry file existence** | The `entry` path must resolve to an existing `.js` or `.mjs` bundle relative to the manifest | [`external-plugins.ts`](https://github.com/opengeos/GeoLibre/blob/main/external-plugins.ts) (throws mismatch error if check fails) |
| **Size limits** | Archive ≤ 50 MiB; individual assets ≤ 2 GiB | [`remote-file-formats.ts`](https://github.com/opengeos/GeoLibre/blob/main/remote-file-formats.ts) and install-time enforcement |
| **Signature verification** | Tauri builds verify exactly one [`plugin.json`](https://github.com/opengeos/GeoLibre/blob/main/plugin.json) exists and is readable | [`src-tauri/src/lib.rs`](https://github.com/opengeos/GeoLibre/blob/main/src-tauri/src/lib.rs) lines 1354-1367 |

Validation failures abort the installation and surface an error dialog to the user. The native Rust validation in [`lib.rs`](https://github.com/opengeos/GeoLibre/blob/main/lib.rs) provides an additional security layer for desktop builds.

## Activation Phase: Runtime Loading

Once validation passes, plugins are activated through dynamic module import.

### Bundled vs. Runtime Activation

- **Bundled drop-ins**: May specify `"activeByDefault": true` in their manifest to appear on startup (documented in [`plugin-api.md`](https://github.com/opengeos/GeoLibre/blob/main/plugin-api.md) line 832)
- **Runtime plugins**: Loaded on demand; the `entry` module is dynamically `import()`-ed and the exported object is inspected

The activation code in [`external-plugins.ts`](https://github.com/opengeos/GeoLibre/blob/main/external-plugins.ts) (lines 421-427) performs version/id/name sanity checks, ensures the exported object implements the `GeoLibrePlugin` interface, and registers the plugin with the internal `PluginRegistry`. Once registered, UI controls appear in [`ManagePluginsDialog.tsx`](https://github.com/opengeos/GeoLibre/blob/main/ManagePluginsDialog.tsx).

## Persistence and Storage

Plugin persistence differs between platforms:

- **Desktop**: Installed plugin folders or downloaded ZIPs are stored under `~/.config/GeoLibre/plugins/`. The registry persists plugin IDs for automatic reloading on subsequent launches.
- **Web**: Remote manifests are refetched each session. Only the manifest URL is persisted in the project file, as browsers cannot write to the filesystem.

## Creating Compatible Plugin Bundles

To build a plugin that GeoLibre can load, structure your ZIP with this layout:

```

my-plugin/
├── plugin.json          # manifest (must be at root)

├── dist/
│   └── my-plugin.js     # entry bundle (export default GeoLibrePlugin)

└── style/
    └── my-plugin.css    # optional stylesheet

```

The `entry` field in [`plugin.json`](https://github.com/opengeos/GeoLibre/blob/main/plugin.json) must point to the JavaScript bundle relative to the manifest location.

## Code Examples

### Minimal plugin.json Manifest

```json
{
  "id": "demo-plugin",
  "name": "Demo Plugin",
  "version": "0.1.0",
  "entry": "dist/demo-plugin.js",
  "manifestUrl": "https://example.com/demo-plugin/plugin.json",
  "minGeoLibreVersion": "2.0.0"
}

```

The manifest must be located at the ZIP root or the shallowest `*/plugin.json` path in the archive.

### Installing a ZIP from the UI

1. Open **Manage Plugins → Settings**
2. Click **Install from file** and select your `.zip` file
3. GeoLibre runs the full validation pipeline; successful plugins appear in the active list

### Programmatic Loading via Internal API

```typescript
import { loadExternalPluginFromUrl } from '@geolibre/plugins';

async function addRemotePlugin(url: string) {
  try {
    const plugin = await loadExternalPluginFromUrl(url);
    console.log(`✅ Loaded ${plugin.id} v${plugin.version}`);
  } catch (e) {
    console.error('❌ Plugin failed to load:', e);
  }
}

// Example usage
addRemotePlugin('https://geolibre.app/plugins/sample/plugin.json');

```

This function fetches, validates, and registers the plugin in a single call.

## Core Implementation Files

| File | Purpose |
|------|---------|
| [`apps/geolibre-desktop/src/lib/plugin-archive-unpack.ts`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/lib/plugin-archive-unpack.ts) | ZIP scanning and [`plugin.json`](https://github.com/opengeos/GeoLibre/blob/main/plugin.json) location |
| [`apps/geolibre-desktop/src/lib/external-plugins.ts`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/lib/external-plugins.ts) | Manifest fetching, validation, and activation |
| [`apps/geolibre-desktop/vite-plugins/bundled-plugins.ts`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/vite-plugins/bundled-plugins.ts) | Build-time bundled plugin loading |
| [`apps/geolibre-desktop/src/components/layout/ManagePluginsDialog.tsx`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/components/layout/ManagePluginsDialog.tsx) | Plugin management UI |
| [`apps/geolibre-desktop/src-tauri/src/lib.rs`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src-tauri/src/lib.rs) | Native Tauri validation layer |
| [`docs/plugin-api.md`](https://github.com/opengeos/GeoLibre/blob/main/docs/plugin-api.md) | Public [`plugin.json`](https://github.com/opengeos/GeoLibre/blob/main/plugin.json) specification |

## Summary

- GeoLibre's **three-phase pipeline**—discovery, validation, activation—ensures only safe, well-formed plugins load
- **ZIP files** are scanned by `locatePluginJsonInArchive` with `__MACOSX/` filtering and nested directory handling
- **Remote URLs** are fetched via `loadExternalPluginFromUrl` and cached by manifest URL
- **Validation enforces** required fields, entry file existence, size limits (50 MiB archives, 2 GiB assets), and native signature checks on Tauri
- **Activation** uses dynamic `import()` of the entry module with interface compliance verification

## Frequently Asked Questions

### What happens if my ZIP has multiple plugin.json files?

GeoLibre selects the **shallowest** [`plugin.json`](https://github.com/opengeos/GeoLibre/blob/main/plugin.json) in the archive hierarchy, preferring root-level manifests. Nested manifests in subdirectories are ignored unless no root-level manifest exists. The `__MACOSX/` folder is explicitly excluded from scanning.

### Can I load plugins from HTTP instead of HTTPS?

No. Remote plugin manifests **must use HTTPS URLs**. This restriction is enforced by the URL validation logic and aligns with GeoLibre's security model for external code execution.

### How do I update a plugin without reinstalling?

For remote plugins, increment the `version` field in your hosted [`plugin.json`](https://github.com/opengeos/GeoLibre/blob/main/plugin.json). GeoLibre re-fetches remote manifests each session and will detect version changes. For ZIP-installed plugins, users must reinstall the updated archive through **Manage Plugins → Settings → Install from file**.