Map Extender
Docs Gallery

Guide

Creating a registry

A registry is a JSON file, published at any URL you control, listing plugins other people can add to their copy of Map Extender. The extension ships with none pre-installed β€” anyone who wants more pastes in a registry's address themselves. This page is the format that file has to follow.

How it fits together

A registry is one JSON document with an array of plugins, each carrying its own code, match patterns, and settings inline. When a user pastes your registry's URL into the extension's gallery, it fetches that one file, shows them what's in it, and lets them add whichever plugins they want β€” each one still goes through the extension's own review screen before it's stored.

The official plugin gallery is itself just a registry published this way β€” its source repository is a working example and reasonable template to start from if you'd rather fork something than write the JSON by hand.

The document shape

{
  "formatVersion": 1,
  "name": "My Plugins",                          // shown in the gallery
  "description": "A short list of my plugins.",  // optional
  "homepage": "https://github.com/you/repo",      // optional
  "updatedAt": "2026-08-17",                      // optional
  "plugins": [
    {
      "id": "my-registry:bicycle-parking",        // namespaced by you, see below
      "name": "Bicycle Parking",
      "description": "Shows bicycle parking from OpenStreetMap.",
      "version": "1.1.0",                         // absent means 0.0.0
      "author": "Jane Doe",
      "homepage": "https://github.com/you/repo",   // optional
      "usage": "Zoom in, then pan to load",        // optional, shown in the popup when enabled
      "screenshots": [                             // optional, https: or data:image/* only
        "https://you.github.io/my-registry/parking.png"
      ],
      "matchPatterns": ["*://*/*"],
      "settingsSchema": [
        { "key": "color", "label": "Marker colour", "type": "text", "default": "blue" }
      ],
      "settings": { "color": "blue" },
      "code": "plugin.mapHook.onHook(function () { /* ... */ })"
    }
  ]
}

The code is inlined directly in the JSON, deliberately β€” one fetch, one thing to validate, and no half-updated state if a follow-up request fails partway through.

Rules the extension enforces

A document failing any of these is rejected, with a message naming the plugin at fault:

  • formatVersion is 1. A document declaring a higher number is refused with a message telling the user to update the extension.
  • Every id is namespaced and unique within the document, and may never start with builtin: β€” that prefix is reserved for the plugins the extension seeds itself.
  • version is major.minor.patch, compared numerically part by part, so 1.10.0 counts as newer than 1.9.0. Bump it whenever code changes, or installs never see the update.
  • code is plain JavaScript for the plugin global described in the API reference β€” no import, export, TypeScript, or JSX. It's injected as a source string, not loaded as a module.
  • matchPatterns follow the same wildcard syntax as a hand-written plugin (<all_urls>, *://*/*, *://*.example.com/*), matched per URL component β€” a bare hostname on its own is not a valid pattern.
  • Every settingsSchema key should have a matching value in settings. Missing ones are backfilled from the schema's defaults on save, but shipping them explicitly keeps the document readable.
  • screenshots must be https: URLs or inline data:image/* β€” anything else is silently dropped, and the user is shown how many images were hidden.

A document is also capped at 2 MB and 200 plugins β€” the extension's local storage is 10 MB total, shared with everything else it stores.

Hosting it

Any https: URL works β€” a GitHub Pages site, a raw file on GitHub, or your own server. The extension re-checks that the response still comes from an https: origin even after following redirects, sends no credentials with the request, and uses ETag caching so refreshing an already-added registry is cheap. Nothing is fetched until a user adds your URL themselves, and nothing refreshes automatically β€” only when they ask.

Fetching discloses browsing

Whoever hosts your registry's JSON β€” and, separately, whoever hosts any https: screenshot URLs in it β€” sees a request each time someone loads it, which discloses that person's IP address and that they're browsing plugins. If that matters to you, embed screenshots as data: URIs instead of linking them.

What protects the user

An installed plugin is executable code running on every site its patterns match, so the extension treats adding one the same way a careful userscript manager would:

  • The source is shown before anything is stored β€” code, match patterns, and settings, behind a plain warning that it runs with the extension's access.
  • Adding never enables. Turning a plugin on for a given site stays a separate choice, made afterward.
  • Updates are never silent. An update is only offered from the same registry URL a plugin was originally installed from β€” so a second, unrelated registry can't hijack it by publishing a plugin with the same id β€” and the user is shown a line-by-line diff of exactly what changed, not just the new code dropped in whole.

That last point is what actually matters: without a diff, "publish something useful, wait for it to be adopted, then push a malicious update" has nothing standing in its way.

Starting from a template

Fork the official registry

github.com/ssz360/map-extender-plugins already has a validation script and a contributing guide for adding a plugin folder β€” forking it and replacing its plugins with your own is the fastest path to a working registry.