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.

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 tier - window.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 3D Map Explorer 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 controls - Plugins:Enabled=false switches the subsystem off; Plugins:AllowedIds allowlists which plugins may load.
  • 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 data already lives in.

Whether it's an in-house analysis tool, a partner data connector, or a custom layer type - package it as one folder, drop it in, and it shows up in the sidebar next to everything else.