A plugin is one folder: a plugin.json manifest plus an optional .NET tier and an optional TypeScript tier. Drop it into the plugins directory and the host discovers and activates it at the next startup — new REST routes, sidebar tools, background workers, and map layer types, with no rebuild of the app. The clean path for utility GIS teams and consultants to ship their own tooling.
Every plugin starts with a plugin.json declaring its id, version, permissions, tools, and tiers. The tiers are optional and independent — a route-only plugin ships a single DLL; a map-widget plugin ships a single ES module. No bundler, no build pipeline: the TypeScript tier is plain ESM served as-is by the in-process server.
IPlugin with a four-hook lifecycle (ConfigureServices, RegisterRoutes, StartAsync, StopAsync); override only what you need.window.Plugins.register(id, { activate, deactivate, layerTypes }) in the map page.host.api field gates activation, so API 1.0 plugins keep working as the SDK grows.# one folder = one plugin my-plugin/ plugin.json # required — describes everything bin/MyPlugin.dll # optional — .NET tier ui/index.js # optional — TS/ESM tier # verify discovery at runtime GET /api/plugins → discovered + active GET /api/plugins/manifests → web-tier catalog GET /api/plugins/my-plugin → full manifest ● [plugins] activated my-plugin v1.0.0
Each tier plugs into a well-defined chokepoint. Plugin tools appear in the sidebar and search automatically; plugin layer types take precedence in the catalog loader.
Mount minimal-API endpoints under /api/plugins/<id>/… from IPlugin.RegisterRoutes. They register alongside every built-in subsystem on the same in-process server.
Declare tools[] in the manifest and bind each id with host.RegisterToolHandler. The button appears in the native sidebar under your chosen category — no UI code required.
IPlugin.StartAsync runs after the server is up — the place for worker loops, sidecar probes, and SSE streams, with StopAsync for cleanup on shutdown or unload.
Register a layer-type factory and any catalog item with your cd_itemtype loads through it — the catalog loader consults the plugin registry before its built-in switch.
host.RegisterCog and host.RegisterSlpk hand outputs to the existing registries, so /api/cog and /api/slpk serve them immediately — no new route needed.
host.PostToMapAsync pushes events from C# to JS ctx.onNativeEvent listeners; ctx.postToNative goes the other way. host.ExecAsync centralizes the JSON-encoding rule.
The loader scans the discovery roots at startup and activates every valid manifest it finds. Installing a plugin never means rebuilding or repackaging VoltGrid itself.
<appdir>/plugins/, shipped with the app.%APPDATA%\3DMapExplorer\LocalMapServer\plugins\, plus an operator override path via the Plugins:Directory appsetting.Plugins:Enabled=false switches the subsystem off; Plugins:AllowedIds allowlists which plugins may load — the governance handle utility IT expects.map:exec, map:addLayer, …). Phase 1 records them; enforcement, signing, and the audit log are Phase 3 roadmap.AssemblyLoadContext.# the in-tree hello-world reference plugin GET /api/plugins/hello-world/ping { "pong": "hello-world", "id": "hello-world", "version": "1.0.0" } # sidebar: 👋 Hello from Plugin · 🔔 Ping Plugin (JS) await window.Plugins.getLayerType('Hello Layer') → factory fn — layer-type registry live
You don't start from a blank page. The repository ships a complete reference plugin and a tutorial-flavored developer guide that walks from a one-route plugin to async jobs and map overlays.
ping route, two sidebar tool handlers, a registered Hello Layer typeappsettings.json, matching host conventions[plugins], [plugin-tool]) in native output and DevToolsTier availability reflects the phased rollout: the .NET and TypeScript tiers and the extension points above ship today; the Python tier, permission enforcement, plugin signing, and a marketplace are roadmap phases. Activation is gated by the manifest's host.api contract version.
Whether it's an in-house vegetation-scoring tool, a connector to a system your utility already runs, or a custom layer type for your network model — package it as one folder, drop it in, and it shows up in the sidebar next to everything else.