Extend the platform. Don't fork it. New

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.

plugin.json manifest.NET tier (IPlugin)TypeScript / ESM tier /api/plugins/<id>/Drop-in installNo app rebuild
One folder, one manifest

Ship a feature as a self-contained package

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.

  • .NET tier — implement IPlugin with a four-hook lifecycle (ConfigureServices, RegisterRoutes, StartAsync, StopAsync); override only what you need.
  • TypeScript tierwindow.Plugins.register(id, { activate, deactivate, layerTypes }) in the map page.
  • Python tier — roadmap (Phase 2): out-of-process FastAPI sidecars, proxied through the in-app server.
  • Versioned contract — the manifest's host.api field gates activation, so API 1.0 plugins keep working as the SDK grows.
plugins/my-plugin/
# 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
Extension points

Hook into the host where it matters

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.

🔌

REST routes

Mount minimal-API endpoints under /api/plugins/<id>/… from IPlugin.RegisterRoutes. They register alongside every built-in subsystem on the same in-process server.

🧰

Sidebar tool buttons

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.

⚙️

Background workers & SSE

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.

🗺️

New map layer types

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.

📦

Artifact registration

host.RegisterCog and host.RegisterSlpk hand outputs to the existing registries, so /api/cog and /api/slpk serve them immediately — no new route needed.

🌉

Native ↔ JS event bridge

host.PostToMapAsync pushes events from C# to JS ctx.onNativeEvent listeners; ctx.postToNative goes the other way. host.ExecAsync centralizes the JSON-encoding rule.

Deployment

Drop the folder in. Restart. Done.

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.

  • Bundled plugins<appdir>/plugins/, shipped with the app.
  • User-installed plugins%APPDATA%\3DMapExplorer\LocalMapServer\plugins\, plus an operator override path via the Plugins:Directory appsetting.
  • Enterprise controlsPlugins:Enabled=false switches the subsystem off; Plugins:AllowedIds allowlists which plugins may load — the governance handle utility IT expects.
  • Declared permissions — manifests declare what they use (map:exec, map:addLayer, …). Phase 1 records them; enforcement, signing, and the audit log are Phase 3 roadmap.
  • Isolated loading — each .NET tier loads into its own collectible AssemblyLoadContext.
http://localhost:5059
# 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
For partners & integrators

Start from a working example

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.

👋

In-tree hello-world plugin

  • All the moving parts in miniature: manifest, a 56-line C# tier, an ESM UI tier
  • One ping route, two sidebar tool handlers, a registered Hello Layer type
  • Proves the toast round-trip and the native → JS event bridge end to end
  • Copy the folder, rename the id — a running plugin skeleton
🧭

Established patterns, documented

  • The canonical async-job pattern — fire-and-forget route, pollable job state
  • The 503-on-unavailable contract for plugins wrapping optional external CLIs
  • Config via appsettings.json, matching host conventions
  • Debug traces ([plugins], [plugin-tool]) in native output and DevTools

Tier 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.

Build on the platform your grid data already lives in.

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.