The composite showed a map and a grid that ignored each other: filtering the grid
to Africa moved its own count 2,107 → 315 while the map beside it kept every pin.
The composite now owns ONE filter state; the children report theirs upward as DOM
events and accept ours downward as attributes, so each stays independently
embeddable — a child mounted alone hears nothing and is handed nothing.
MEASURED on a served built tree, region facet:
grid → map 2,107 → 315, and the map's own state follows (externalRegion and
selectedRegion both 'africa'); clearing returns both
map → grid the map's map-filtered contract → the grid's list 2,107 → 315;
clearing returns it
standalone grid alone 24 rows / Total 2,107 / 0 errors; map alone canvas 1 /
0 errors — unchanged
MUTATION disabling the composite's push leaves the grid filtering itself
(315) while the map stays empty, and kills the reverse direction:
the check fails when the link is broken
Four things this cost, each of which first looked like it worked:
1. StackLayout applies `attrs` ONCE at create — its documented contract. The
first version recomputed `sections`; the composite adopted the filter and the
map's attribute in the DOM never changed. A computed nobody re-reads is not a
channel, so the state is now PUSHED onto the mounted children.
2. The echo cancelled itself. Composite pushes → the grid re-filters → it emits
groups-filtered with activeFilters still EMPTY → the composite adopts that as
"the reader cleared it" and wipes what it just applied. Both handlers were
correct; the round trip undid itself in under a second. A push is not a user
action, so the push window ignores children.
3. The grid ignored host filters BY DESIGN when its own dropdowns are visible:
activeFilters picks local OR props, never both. Right for the desktop
composite, which hides them; silently wrong here. A host facet now seeds the
LOCAL state, so the map's selection appears as a chip in the grid's own bar
and the reader can clear it there.
4. That seeding called lbl(), which is local to another computed — "lbl is not
defined", watcher dead, attribute set, list unchanged. Visible only after the
probe was widened to console.error. A silent watcher failure looks exactly
like a feature that was never wired.
SCOPE, DELIBERATE: region only. The map filters religion and language by the
canonical strings its own data carries; the grid filters them by slugs from a
different table. Linking those means inventing a mapping, and a wrong link is
worse than an honest gap.
The map also gains a verification handle (window.__researchMapFilterState), the
same contract as __rdashMap and __grMap: a linked filter is a STATE claim, and
every DOM proxy for it was either tab-specific or a selector guess.
|
||
|---|---|---|
| .claude | ||
| .forgejo/workflows | ||
| .github/workflows | ||
| _archive/staging-surface-2026-09-22 | ||
| data | ||
| deployment | ||
| documentation | ||
| internal | ||
| library | ||
| plugin-designs | ||
| public | ||
| serving | ||
| skills/make-a-doxa-map | ||
| .env.example | ||
| .gitattributes | ||
| .gitignore | ||
| AGENT-BRIEF.md | ||
| bun.lock | ||
| capacitor.config.json | ||
| LICENSE | ||
| package.json | ||
| README.md | ||
| vite.config.js | ||
| vite.doxa-maps-build-config.json | ||
dx-maps — one build, maps that embed everywhere
Write a map once in plugin-designs/<your-map>/, run one build, and drop it on any site —
iframe, script embed, mobile app — or open every map at once as one app. Five plugin
capabilities, five deployment methods, one shared library, one build.
| What dx-maps set out to do | Proof |
|---|---|
| One build → one folder you can drop on any host | bun run check:drop |
| Every map embeddable from a copy-paste snippet | deployment methods · bun run check:embed-targets |
| Every map interoperable through props and events | bun run verify:interop |
| One app nests every map — and each stays embeddable on its own | capability 5 · bun run check:composite-reg |
| 5 capabilities × 5 deployment methods | bun run verify:5-caps |
| Shared library, private plugins — reuse without breaking each other | bun run check:library-index |
One agent per plugin, one shepherd, one green main |
agent swarm |
The 5 plugin capabilities
A capability is how much a plugin nests, from one element (1) to an app of apps (5). Every screenshot is a real bundle from this repo's build.
Capability 5 — app shell: the Research Gallery
One plugin, one element (<doxa-maps-spa>), and every tab inside it is a real plugin from a lower
tier that you can still embed on its own. plugin-designs/doxa-maps-spa/
What the Research Gallery is made of — seven production maps, an Experimental shelf, all five capabilities in one plugin
Research Gallery <doxa-maps-spa> capability 5
├─ Dashboards
│ ├─ Big Picture dashboard-plus-grid capability 4
│ │ ├─ research dashboard research-dashboard capability 3
│ │ └─ people-groups grid people-groups-grid capability 2
│ ├─ Simple Research research-map-plus-grid capability 4
│ │ ├─ research map doxa-research-map capability 3
│ │ └─ people-groups grid people-groups-grid capability 2
│ └─ Regions regions-mfe capability 4
│ ├─ regions list regions-list capability 1
│ └─ region dashboards region-dashboards capability 1
├─ Prayer Mobilization
│ ├─ UPG-100 List upg-100-mfe capability 4
│ │ ├─ list upg-100-list capability 1
│ │ └─ poster wall poster-wall capability 1
│ └─ Prayer Progress doxa-simple-map capability 2
├─ Gospel Research
│ ├─ Cities with Internet connectivity-map capability 1
│ └─ Gospel Resources resources-map capability 1
└─ Experimental (a folded shelf, filled automatically from manifest.json)
├─ prayer walk planner prayer-walk-planner capability 1
├─ UPG neighbor upg-neighbor capability 3
├─ all people groups all-people-groups capability 1
├─ country posters country-posters capability 4
└─ vision card doxa-vision-card capability 2
Capability 4 — composite: plugins wired together
Nests lower-tier plugins and wires them with props and events. plugin-designs/my-upg-100-list/
| List (cap 1, embeddable alone) | Poster wall (cap 1, embeddable alone) |
|---|---|
![]() |
![]() |
Capability 3 — multi-view: many views, one element
plugin-designs/doxa-research-map/
Capability 2 — presets: one plugin, many maps
One root.vue plus a preset table. plugin-designs/doxa-simple-map/
| Prayer | Engagement | Adoption |
|---|---|---|
![]() |
![]() |
![]() |
Capability 1 — single element
One folder, one .vue, one element. plugin-designs/all-people-groups/
Which bundle sits at which tier is generated, not remembered: documentation/reference-capability-exemplars.md.
The 5 deployment methods
1. iframe — the prayer map on any site ✅ production
One <iframe> tag, no build step. Each plugin's exact snippet is embed.iframe in manifest.json:
<iframe src="https://YOUR_HOST/doxa-maps/doxa-simple-map/doxa-simple-map/prayer/index.html?utm_source=YOUR_SOURCE"
width="100%" height="600" style="border:none;border-radius:12px;" allowfullscreen></iframe>
2. Mobile app — UPG-100 on a phone ✅ built
The UPG-100 prayer lists and posters, shipped on their own as a Capacitor app: one poster per row,
print options that wrap to fit. bun run check:mobile · bun run phone (needs Android SDK + JDK 17)

3. JavaScript embed — the Research Gallery on dx-life ✅ production
The whole capability-5 app on the dx-life research page, from one <script src> and one element —
replacing the separate research map, list and regions dashboards the page used to carry. The exact
snippet is embed.script in manifest.json; copy its integrity hash from there, as it changes with
every build:
<script>window.MAP_APP_API_URL="https://pray.doxa.life";</script>
<script src="https://YOUR_HOST/doxa-maps/doxa-maps-spa/doxa-maps-spa.js"
integrity="sha384-…" crossorigin="anonymous"></script>
<doxa-maps-spa profile-config='{"profile":"doxa-maps-spa","tk":"YOUR_MAPBOX_TOKEN","utmSource":"YOUR_SOURCE","assetsBase":"https://YOUR_HOST/doxa-maps/assets"}'></doxa-maps-spa>
4. WordPress / Disciple Tools plugin — 🧪 proof of concept
A shortcode + block plugin in deployment/wordpress/; its contract test passes
(bun run check:wp-embed). Proven, not production-ready.
5. Desktop app (Tauri) — 🧪 proof of concept
A desktop binary and .deb build from deployment/src-tauri/ (bun run check:tauri). Proven,
not production-ready.
Mapbox token: the dev server bakes one in; built pages take
?tk=pk.…, a pasted token, or a token endpoint. Or skip Mapbox entirely:provider: "maplibre"on any instance renders on MapLibre with no key. Every command and prerequisite: documentation/README-SECTION-3-cli-commands.md.
Build it with an agent swarm
Every plugin has one agent responsible for building it, working in its own git worktree. The
plugins live together in the Plugin Studio — one repo, one shared library — and one shepherd
makes sure every agent is doing its job well. Changes land on main through a merge queue (the
model behind GitHub's merge queue and GitLab's merge trains): one at a time, rebased and gated.
# one worktree + one agent per plugin
git worktree add -b regions/work ../dx-maps-regions main
git worktree add -b upg100/work ../dx-maps-upg100 main
# one dev server per agent, each on its own port
bun x vite --port 5301 --strictPort
# land through the queue: one at a time, rebased, gated
flock /tmp/dx-maps-main.lock sh -c '
git fetch origin && git rebase origin/main &&
bun run gates:pre && git push origin HEAD:main'
One agent owns the full build (it wipes the shared output first); everyone else checks their own
bundle with BUNDLE=<name> bun x vite build. How to contribute, and the bar for shared code:
documentation/CONTRIBUTING.md.
Diagram source: delegation-model.svg.
The Plugin Studio — ★ where you work
Every plugin in one place, sorted by capability. Pick any plugin to run it live, see what it is composed of, and copy its embed code. Tell your agent which plugins to group, and it composes them into a higher-capability plugin with props, events and a layout — that is how the Research Gallery was built. (The studio is being redesigned; this screenshot tracks it.)
Get it running
You need git, Bun and Node ≥ 18.
git clone https://git.nuxtbase.app/doxa-open/doxa-maps.git
cd doxa-maps
bun install --frozen-lockfile
bun run dev # → http://localhost:5173/ is the Plugin Studio
No build needed first — the built tree is committed. To start a plugin, copy
plugin-designs/template-bundle/ to plugin-designs/<your-map>/; the studio finds it automatically.
| Command | What it does |
|---|---|
bun run dev |
The Plugin Studio, hot-reloading, at http://localhost:5173 |
BUNDLE=<name> bun x vite build |
Build one bundle only |
BUILD_ALLOW=1 bun run build |
The full deployable tree in public/doxa-maps/ (~4 min; refuses without the flag, to protect a shared tree) |
bun run serve |
Serve the built tree at http://localhost:4173 |
bun run check:settled |
Exit 0 = the built tree is safe to measure right now |
bun run gates:pre · bun run gates:post |
The verification gates |
bun run shots |
Regenerate these screenshots (ONLY=<name> for one) |
Repository layout
plugin-designs/ YOUR maps — one folder per plugin, a free zone
library/ the SHARED toolkit (layouts, components, composables) — a high bar
public/doxa-maps/ the build output: the drop-anywhere tree + manifest.json
deployment/ Capacitor · WordPress · Tauri
internal/ the build generator and the gates
documentation/ tutorial · how-to · reference · architecture — start at documentation/README.md
Maps should be open source, run everywhere, and multiply. One build makes each map a self-contained file for your site, your partners' sites and a phone. The Plugin Studio is where you compose them into something bigger.












