# How External Plugins Load in GeoLibre Desktop vs Web Build: Security Model Explained

> Discover how GeoLibre loads external plugins across desktop and web builds. Understand the security model and plugin trust for GeoLibre.

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

---

**External plugins in GeoLibre load through a unified core system that adapts to each platform—desktop supports ZIP archives and remote URLs via Tauri's filesystem API, while the web build supports only remote URLs with no-op filesystem operations; both enforce a manifest-based trust model where untrusted plugins are quarantined until explicitly approved by the user.**

GeoLibre's plugin architecture distinguishes between **built-in plugins** shipped with the application and **external plugins** that users add at runtime. Whether you're running the Tauri-based desktop build or the browser-based web build, the core loading logic lives in [`apps/geolibre-desktop/src/hooks/usePlugins.ts`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/hooks/usePlugins.ts). This article breaks down how external plugin loading differs by platform and how the security model protects users from untrusted code.

## Built-In Plugin Loading Foundation

Before examining external plugins, understanding the built-in system provides necessary context.

Built-in plugins reside in the `packages/plugins` workspace and export from [`packages/plugins/src/index.ts`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/index.ts). The Vite plugin [`bundled-plugins.ts`](https://github.com/opengeos/GeoLibre/blob/main/bundled-plugins.ts) scans `apps/geolibre-desktop/public/plugins/` at build time, generating a virtual module:

```ts
import { bundledPluginManifestPaths } from "virtual:bundled-plugins";

```

The [`usePlugins.ts`](https://github.com/opengeos/GeoLibre/blob/main/usePlugins.ts) hook iterates these manifest URLs, fetches each, converts responses to Blob URLs, and dynamically imports them with `import(/* webpackIgnore: true */ blobUrl)`. Each imported plugin registers with the central `PluginManager` via `manager.registerAll([...])`.

This Blob-based dynamic import pattern—critical for isolation—carries over to external plugins on both platforms.

## Desktop Build: Full Filesystem and Remote Support

The Tauri-based desktop build offers the complete external plugin feature set. The relevant functions import into [`usePlugins.ts`](https://github.com/opengeos/GeoLibre/blob/main/usePlugins.ts) from [`apps/geolibre-desktop/src/lib/external-plugins.ts`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/lib/external-plugins.ts):

```ts
import {
  installWebPluginArchive,
  listInstalledWebPlugins,
  loadExternalPlugins,
  reloadExternalUrlPlugin,
  resolvePluginAssetUrlForLoadedPlugin,
  uninstallWebPlugin,
  unloadFilesystemPlugin,
  unloadRemovedUrlPlugins,
} from "../lib/external-plugins";

```

### ZIP Archive Installation

The `installWebPluginArchive` function handles desktop-only ZIP installation:

- Extracts [`plugin.json`](https://github.com/opengeos/GeoLibre/blob/main/plugin.json) manifest from the archive
- Validates manifest structure and required fields
- Copies plugin files to the user's data directory via Tauri's filesystem API
- Records the plugin as "installed" in local state

After installation, `loadExternalPlugins()` reads stored manifests and applies the trust partition.

### Trust Partitioning Before Execution

The desktop build implements `../lib/plugin-trust` to separate **trusted** from **untrusted** plugins before any code executes. The `partitionProjectPluginManifestUrls` function categorizes manifests based on:

- Previously approved URLs stored in user preferences
- New URLs requiring explicit user review

Only **trusted plugins** proceed to dynamic import. Untrusted plugins populate a **quarantined list** visible in the [`ProjectPluginTrustDialog.tsx`](https://github.com/opengeos/GeoLibre/blob/main/ProjectPluginTrustDialog.tsx) UI. Users must explicitly grant trust—untrusted plugins never run.

```tsx
// Desktop: Installing and loading a plugin from ZIP
await installWebPluginArchive(zipFilePath);
await loadExternalPlugins(); // trust check happens here

```

## Web Build: Remote-Only with Restricted Filesystem

The pure web build uses the identical API surface but with **filesystem functions as no-ops**. Browser security policies prevent writes to local directories, so:

- `installWebPluginArchive` → no-op
- `unloadFilesystemPlugin` → no-op
- File-based plugin persistence → unavailable

Remote URL loading remains fully functional. The `loadExternalPlugins` function fetches plugin archives over HTTPS, validates manifests, and imports via Blob URLs—exactly as on desktop. The trust enforcement layer operates identically: untrusted remote plugins remain disabled until user approval through the same [`ProjectPluginTrustDialog.tsx`](https://github.com/opengeos/GeoLibre/blob/main/ProjectPluginTrustDialog.tsx).

```tsx
// Web/Desktop: Loading a remote plugin by URL
await reloadExternalUrlPlugin('https://example.com/my-plugin/manifest.json');
// User approves manifest URL in trust dialog
await loadExternalPlugins();

```

## Security Model: Four-Layer Protection

GeoLibre implements a **sandboxed plugin trust model** with these defenses:

### 1. Manifest-Based Trust Anchoring

Every external plugin requires a [`plugin.json`](https://github.com/opengeos/GeoLibre/blob/main/plugin.json) manifest. The manifest URL serves as the trust anchor—`partitionProjectPluginManifestUrls` tracks approval per-URL, not per-plugin-version, preventing substitution attacks.

### 2. User-Controlled Trust Decisions

No external plugin executes without explicit user approval. The [`ProjectPluginTrustDialog.tsx`](https://github.com/opengeos/GeoLibre/blob/main/ProjectPluginTrustDialog.tsx) presents:

- Manifest origin and requested permissions
- Approve/Revoke actions with persistent storage

Untrusted plugins load in a **disabled state** with zero code execution.

### 3. Blob URL Isolation

Dynamic imports use `import(/* webpackIgnore: true */ blobUrl)` construction. This technique:

- Isolates plugin code from the main application bundle
- Enables runtime-controlled lifecycle (unload via URL revocation)
- Prevents plugins from interfering with core module resolution

### 4. Defensive Error Handling

The `reportPluginError` mechanism in [`usePlugins.ts`](https://github.com/opengeos/GeoLibre/blob/main/usePlugins.ts) catches and routes failures to the diagnostics panel. Exceptions during plugin initialization or command execution cannot crash the host application.

## Key Files and Responsibilities

| File | Location | Purpose |
|------|----------|---------|
| **usePlugins.ts** | `apps/geolibre-desktop/src/hooks/` | Central hook for plugin registration, external loading, and `PluginManager` access |
| **bundled-plugins.ts** | `apps/geolibre-desktop/vite-plugins/` | Build-time scan of `public/plugins/` generating `virtual:bundled-plugins` |
| **external-plugins.ts** | `apps/geolibre-desktop/src/lib/` | Installation, loading, unloading, and trust handling for external plugins |
| **plugin-trust.ts** | `apps/geolibre-desktop/src/lib/` | Trust partitioning and user decision tracking |
| **ProjectPluginTrustDialog.tsx** | `apps/geolibre-desktop/src/components/layout/` | UI for approving/rejecting external plugin manifests |

## Platform Comparison Summary

| Capability | Desktop (Tauri) | Web Build |
|------------|---------------|-----------|
| ZIP archive installation | ✓ Full support | ✗ No-op (browser restriction) |
| Remote URL loading | ✓ Full support | ✓ Full support |
| Local filesystem persistence | ✓ Tauri API | ✗ Unavailable |
| Trust dialog enforcement | ✓ Identical | ✓ Identical |
| Blob URL isolation | ✓ Identical | ✓ Identical |

Both builds converge on the same security posture: **external code requires explicit user trust and executes in an isolated, revocable sandbox.**

## Summary

- **Desktop builds** support full external plugin lifecycle—ZIP installation, filesystem persistence, and remote URLs—via Tauri's native APIs
- **Web builds** support only remote URL loading with filesystem operations disabled, maintaining API compatibility through no-op stubs
- **Security model** enforces manifest-based trust with user approval, Blob URL isolation, and defensive error handling across both platforms
- **Core implementation** lives in [`usePlugins.ts`](https://github.com/opengeos/GeoLibre/blob/main/usePlugins.ts) with platform-specific behavior delegated to [`external-plugins.ts`](https://github.com/opengeos/GeoLibre/blob/main/external-plugins.ts) functions
- **Untrusted plugins** are quarantined and never executed, with revocation possible at any time

## Frequently Asked Questions

### How does GeoLibre prevent malicious plugins from running automatically?

Plugins require a valid [`plugin.json`](https://github.com/opengeos/GeoLibre/blob/main/plugin.json) manifest and explicit user trust approval through [`ProjectPluginTrustDialog.tsx`](https://github.com/opengeos/GeoLibre/blob/main/ProjectPluginTrustDialog.tsx). The `partitionProjectPluginManifestUrls` function in [`plugin-trust.ts`](https://github.com/opengeos/GeoLibre/blob/main/plugin-trust.ts) separates untrusted manifests before any dynamic import occurs. Untrusted plugins remain in a disabled state with no code execution path.

### Can web users install plugins from ZIP files like desktop users?

No. Browser security policies prevent local filesystem writes, so `installWebPluginArchive` and related filesystem functions are no-ops in the web build. Web users can only load plugins from remote HTTPS URLs that pass the same trust verification process.

### What happens if a trusted plugin becomes untrusted or is revoked?

The `PluginManager` tracks loaded plugins by their Blob URLs. When trust is revoked, `unloadRemovedUrlPlugins` or direct URL revocation removes the plugin from the active set. The plugin's code is no longer reachable through its Blob URL, and subsequent `loadExternalPlugins` calls exclude it from registration.

### Where is the plugin trust state persisted across sessions?

User trust decisions for manifest URLs are stored persistently and retrieved by `partitionProjectPluginManifestUrls` during each `loadExternalPlugins` invocation. The exact storage mechanism (Tauri store for desktop, browser storage for web) is abstracted by the trust layer, with the same approval UI presented regardless of platform.