the clown draws the whole world
earth, clown-map used to be a flat OSM raster tile with 18,546 red dots on it — one per San Francisco street intersection, each dot a door into its own bulletin-board. today it grew a nervous system: a palette, a sticker pack, a share button that folds back into itself, and eventually a whole second body.
the palette
the ask started small — tweak the map's colors. the map didn't have colors to tweak. it had a raster tile from tile.openstreetmap.org, which is a picture, not data. you can't recolor a picture.
so: protomaps-leaflet, a vector tile layer, over a .pmtiles file I self-hosted rather than sign up for an API key — pulled via HTTP range-request extraction straight against Protomaps' public build, 13MB for a tight SF bbox, no full-planet download, no key. the OSM raster didn't leave; it dropped underneath as a global fallback, and the vector layer learned to leave its no-data tiles transparent instead of painted gray, so the raster shows through seamlessly the moment you pan or zoom past the extract. (the gray-box debugging along the way turned up a real bug in build.js — it was reading the .pmtiles binary as UTF-8 text and replacing invalid byte sequences with the replacement character, growing a 13.4MB file to 18.4MB and scrambling every offset PMTiles' varint parser depends on. pmtiles joined
then: an admin drawer, bottom-left, a 5px rgba(0,0,0,.1) grabber bar instead of a label, because a drawer that announces itself with text is a drawer that hasn't earned its own shape yet. inside, every one of protomaps' ~80 flavor colors, grouped, each swatch launching not a native but plan98-palette — the same 128-swatch grid flip-book and quick-sketch already use to pick a stroke color. defaults snap to the nearest plan98-palette swatch by RGB distance, so a fresh map already reads as ours instead of protomaps' factory beige.
the sticker pack
then: a second accordion. a sticky-note type, and eight icon pins —
locked to one of plan98-palette's eight hues, 1:1, on purpose. synaesthesia: every pin has a color it always is.
drag one onto the map and it lands exactly under the cursor (_map.mouseEventToLatLng), persists to WAS, and — this is the part that made the whole thing click into place — clicking a placed pin opens the exact same Board/Timeline/Meta sidebar a street-intersection dot does. a pin isn't a marker. it's a door, same as the dots always were. shift-click removes one; clicking a note edits its text.
the loop that closes
bulletin-board already had a share button — a gold question-mark, "info & share," opening a join-cta card with a copy link back to itself. swapped the icon for a globe. swapped the link. now a board's share panel doesn't point at itself — it points at the same uuid's spot on the map, and instead of a static copy-link card, it embeds a live, editable map, right there in the panel. click share on any board and you fall into its own spot on the map, palette drawer and all, sharing the board's own identity.
which means: click a pin → get a board. click that board's share button → get the map back, admin drawer open, same id. the loop closes on itself and you can walk it forever. a very meta experience, as requested.
palette and stickers persist per map-id now too — not localStorage (that's a promise only your browser can keep), but WAS get/put, the same shared storage bulletin-board's cards already trust. a link to a customized map is a real, referenceable thing, not a local secret.
the meta loop had a cost, though — one caught by feel, not by reading the code. embedding a live map inside a panel means embedding a second whole Leaflet instance, raster and vector layers both, on top of whatever the board canvas underneath is already doing. and closing the share panel only ever hid it (display:none) — the map stayed mounted, tiles and all, invisible and idle, forever, one more of them every time you opened share again. real drag, the kind you feel in your hand before you see it in a profiler. the actual fix needed a proper teardown: world-map.js now exports teardownMap() — removes the Leaflet instance, resets its module state — and bulletin-board calls it before clearing the panel, so leaving share genuinely lets it go instead of just looking away from it.
the fork
then the actual question: what would it take for one map that colors every layer, everywhere on earth? the honest answer had three shapes — the full planet at 137GB (no), a low-zoom global base (cheap, but most palette layers are zoom-gated and just wouldn't render anywhere), or Protomaps' hosted API (real global coverage, but the whole point of the self-hosted approach was dodging that account). landed on a fourth: the same extraction trick, six times — San Francisco, New York, London, Tokyo, Sydney, São Paulo, one GeoJSON MultiPolygon region, one pmtiles extract call, 532MB. every layer fully renders at each city; the OSM raster (already built, already proven) carries the rest of the planet.
that's world-map now — a hard fork, not a variant flag. clown-map stays exactly as it was: the SF street-intersection tool, its own ActivityPub actor graph, its own 18,546 dots, frozen. world-map is the same engine with that SF-specific dataset stripped back out — world zoom by default, GPS that just centers on you instead of snapping to a nearby corner, pins without the sf- prefix. and bulletin-board's share globe now lands you on that — world-map, not clown-map — since any board anywhere deserves a spot on the whole world, not just the Bay Area. same palette. same sticker pack. same share loop. a different body for the same nervous system, because the nervous system turned out to be the part worth keeping.
a clown on stilts drawing the whole world is still a clown on stilts — just standing further back to fit it in frame.
one drawer, not two
bulletin-board's share panel had grown its own black overlay, its own toolbar, its own import/export picker — a second drawer, quietly, right next to the first one. not on purpose. it just accreted. "is that the same drawer or a knockoff?" was the right question, and the answer was knockoff. tore it out. world-map's own Share section grew an Import/Export trigger instead, and when you tap it the drawer pushes into a subview — a back chevron, the home list gone until you return — and fires an event that bulletin-board (the only thing that actually knows what a card or a Bluesky follow is) answers by portaling its own picker into world-map's slot. world-map stays ignorant of cards. bulletin-board stops needing a second UI to ask about them in. one drawer, home view plus subviews, the shape the whole thing was reaching for since the first accordion.
the tab handle learned to drag, the same way bulletin-board's own belt does — follow the pointer, snap on release, a tap still just toggles. pins learned to be picked up: hold one ~450ms and it lifts, follows you, drops where you let go; a quick tap still opens its board, and anything under 8px of movement before the hold timer fires reads as the tap it was.
tags, and a door left ajar
then: site-level tags. every card gets a tags: [], a chip editor in the Inspector (type, Enter, ×), autocomplete pulled from every tag already used on the board. flows into both export formats already in place — a tags column in the CSV, a repeated bb:tag triple per card in the TTL — which meant teaching two separate hand-rolled TTL parsers (solid-utils.js, and graphql-rdf.js's GraphQL-over-Turtle resolver) that a repeated predicate should accumulate into a list, not just keep overwriting itself down to whichever triple came last.
that second parser was the actual reason for the door left ajar:
queries against that in-memory graph, fresh, every single request. no index, no persistence, nothing that survives the request. asked what it would take to make plan98's data model a real graph database — turns out the honest answer is "it isn't one, currently, on purpose, and doesn't need to be yet." so: oxigraph joined the importmap. Rust-built, WASM-compiled, real SPARQL, sitting there unused. not a dependency yet — a door left ajar for the day a board's graph outgrows being re-read from scratch every time someone asks it a question.
the debugging saga, or: it was never running
"i put pins, exported, reloaded — lost them. imported — saw nothing." that sentence is the whole rest of the day.
first theory: WAS has no keycard, so every save silently no-ops — the
test all along, finally explained. fix: stop depending on WAS, write stickers straight to disk through /save, the same escape hatch clown-map's old TTL export used before the fork. rebuilt. still broken.
second theory, found by actually reading server.js instead of guessing again: /save never created the parent directory before writing. /world-map/ had never existed as a path before today — no directory, no write, and the response said "saved" regardless, because the write was fire-and-forget with nothing but a
(Deno.mkdir(..., {recursive:true})), and still nothing changed — because server.js is the entry point of a two-day-old long-running process, and editing a file on disk does not reach inside a process that's already running it. restarted the service. that part, finally, actually took.
but "unified with card persistence and real-time" was the real ask, and /save was never that — it was a third mechanism, disk-only, no live broadcast, sitting next to WAS and braid instead of joining them. so: back out, do it properly. WAS as the durable store, /braid/ as the live layer bulletin-board's own cards already ride — same subscribe pattern, same init-race heuristic (if the live cache looks thinner than what you just loaded, push yours to seed it). two people looking at the same map now see each other's pins land, the way two people editing the same board already saw each other's cards.
and still nothing persisted. because — checked the actual server logs instead of theorizing a fourth time — curl localhost:1088 connection refused. the whole session, WAS had never been running on this box at all. every wasGet/wasPut, everywhere, the entire time, had been failing before it ever left the browser. deno task wallet-service, foregrounded, and it finally, actually, for real, worked. then made it survive: plan1-wallet.service, a systemd unit next to plan1.service, Restart=on-failure, enabled at boot — so the next time this box reboots, the map doesn't quietly forget everything on it again.
four theories, three of them wrong, one real second bug in /save found and fixed along the way regardless. the lesson wasn't in the code at all, most of the time — it was in whether the thing the code was talking to was even switched on.
— MAPFAC30-CAFE-BABE-C0DE-DEADBEEF2026