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

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 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 manifest at the root
  • Unpacked directories with the same structure, typically used during plugin development
  • Remote HTTPS URLs pointing directly to a 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. The helper function locatePluginJsonInArchive scans ZIP entries and returns the path of the first 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 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). 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 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 lines 122-124
Required fields id, name, version, and entry must be present; manifestUrl required for remote plugins 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 (throws mismatch error if check fails)
Size limits Archive ≤ 50 MiB; individual assets ≤ 2 GiB remote-file-formats.ts and install-time enforcement
Signature verification Tauri builds verify exactly one plugin.json exists and is readable 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 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 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 (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.

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 must point to the JavaScript bundle relative to the manifest location.

Code Examples

Minimal plugin.json Manifest

{
  "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

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 ZIP scanning and plugin.json location
apps/geolibre-desktop/src/lib/external-plugins.ts Manifest fetching, validation, and activation
apps/geolibre-desktop/vite-plugins/bundled-plugins.ts Build-time bundled plugin loading
apps/geolibre-desktop/src/components/layout/ManagePluginsDialog.tsx Plugin management UI
apps/geolibre-desktop/src-tauri/src/lib.rs Native Tauri validation layer
docs/plugin-api.md Public 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 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. 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.

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 →