ADR 0001 — A plugin-first kernel, not a map library with a plugin API

Status: accepted · Amends: — · Amended by: —

See also ADR 0017, which records the second mechanical gate on these same package boundaries, and ADR 0016, which is what the versioning consequence below depends on.

Context

Every mapping library in the JavaScript ecosystem starts small and ends up with a map.enableSnapping(). The pattern is consistent and the reason is understandable: a concrete feature is easy to ship, an extension point is not, and the first user who asks for snapping is asking for snapping, not for a snap-provider interface.

The cost arrives later, and it is paid by everyone. The moment enableSnapping() exists, the library has decided what snapping means — for every user, for every domain, forever. The cadastral user who needs to snap to a parcel corner registered on a server, and the game user who needs to snap to a hex centre, both have the same two options: fork, or ship a patch that nobody will accept because it makes the API worse for the other one.

We needed one kernel to serve a land registry, an urban-planning tool and a game level editor. Those three have almost nothing in common except geometry: one has geodesy and legal consequences, one has geodesy and no legal consequences, and one has no Earth at all.

Decision

The core owns five extension mechanisms:

  1. a typed event bus with cancellable before: hooks,
  2. a plugin registry,
  3. two middleware pipelines (sync interaction, async commit),
  4. a command bus,
  5. a feature store (with its spatial and topology indexes).

— plus the small set of services those mechanisms cannot work without and no plugin may replace: the CRS service (ADR 0005), the tool manager (ADR 0010), the layer manager (ADR 0008), the validation registry, theme and i18n. packages/core/src/BlaeuMap.ts carries the full list, and the test for admitting anything to it is whether an extension mechanism above would be inoperable without it.

Everything a user would call a feature — drawing, snapping, editing, measurement, selection, undo/redo, topology validation, UI chrome, and even layer types — is a plugin that registers through an extension point the core defines and the core calls. The core never imports a plugin. CI enforces it mechanically:

npm run scaffold:check    # the manifests the next line reads are generated, not hand-written
npm run lint:boundaries   # fails on core→plugin, plugin→plugin, core-as-dependency, missing-core-peer

Both gate npm run verify, and in that order, because a boundary check is only as good as the manifest it reads — see ADR 0017.

Domains are presets: composable plain-data bundles of plugins, config, layers, rules, theme and messages (see ADR 0006).

The test we hold ourselves to: when a plugin needs something the core does not offer, the correct response is "the core is missing an extension point," never "add a small thing to the core."

Alternatives rejected

A monolithic library with a plugin API bolted on (the Leaflet / OpenLayers / Mapbox Draw lineage). Cheaper to start, and it is the reason preset-game would have been impossible: a monolith with drawing and measurement built in has already assumed a basemap, a geodetic surface and a @turf/area, none of which a game world has. You cannot subtract a core feature. You can decline to install a plugin.

Inheritance — class CadastreMap extends BlaeuMap. Domains would override behaviour by subclassing. Rejected because it composes exactly once: a municipality that wants the national cadastre customisation and a utilities extension has to pick a base class, and the two hierarchies do not merge. Composition of data (presets) has no such ceiling.

A registry of hooks without a command bus or pipelines — i.e. events only. Rejected because events cannot modify what happens; they can only observe it. Snapping needs to rewrite the pointer position, and validation needs to veto a write. Both require a chain that owns the value, which is a pipeline, not an event.

Consequences

Edit this page on GitHub — this site is generated from docs/adr/0001-plugin-first-kernel.md, which is the source of truth.