5 KiB
Capabilities and targets
Two separate questions about a plugin: how composed is it (capability 1–5) and where can it
run (targets). Only the first is enforced.
Capability 1–5
Pick the lowest level that does the job.
| Level | type |
What it is | Example |
|---|---|---|---|
| 1 | single |
one screen, one element | plugin-designs/prayer-walk-planner/ |
| 2 | parameterized |
one screen, several named presets from <screen>.instances.json |
plugin-designs/doxa-simple-map/ |
| 3 | monolithic |
many views fused inside one element | plugin-designs/doxa-research-map/ |
| 4 | nested-elements-mfe |
nests other elements, each still embeddable alone | plugin-designs/dashboard-plus-grid/ |
| 5 | nested-mfe-spa |
nests whole apps | plugin-designs/doxa-maps-spa/ |
It is DERIVED, and your declaration is CHECKED against it
Levels 1 and 2 are inferred: a bundle with an *.instances.json is parameterized, otherwise
single (internal/plugin-gallery/discover.mjs:44-52). Levels 3–5 you declare in
<screen>.meta.json "type".
Declaring is not the end of it. internal/check-capability-derived.mjs derives the capability
from what your plugin actually composes and fails when an explicit type disagrees. Its
own header states the contract:
A profile that composes anything derives cap-4, or cap-5 if it composes a cap-4 — and an EXPLICIT type must AGREE with that. The declaration stays authoritative; it is not overridden, it is checked.
The defect it exists for: research-dashboard nested ten views with no meta.json and shipped as
capability 1, because a missing file is indistinguishable from a deliberate choice. Every surface
that shows a tier — the manifest, the gallery, the studio rail — reads that one field, so the
understatement propagated everywhere.
Run bun run check:capability-derived. Two related gates: check:capability-citations and
check:capability-exemplars.
Two traps the gate documents
Worth knowing because they will bite you the same way:
composesis per-profile at source but hoisted to the bundle in the manifest. Deriving from the bundle row applies a composite's edges to its plain sibling leaves — gospel-resources has one composite and four single leaves, and all four wrongly derived as cap-4.- Refs appear in three forms in the same field:
'bundle/profile', a bare'bundle', and an element tag. Resolving only the first demoted plugin-studio from 5 to 4.
targets — where the plugin may run, and it has teeth
A <profile>.meta.json may carry "targets", naming where the plugin is meant to run:
{ "type": "single", "label": "Rudder", "targets": ["desktop"], "backend": "backend/rudder-engine" }
Omitting it means "everywhere", and that is the right default for almost every plugin. The whole point of the build is one folder that runs as a web embed, a mobile app and a desktop app.
Declaring it has a real consequence, so declare it only when you mean it:
-
internal/profile-meta.mjs:28reads the field (string entries only) and:36carries it into the manifest, so every consumer of the manifest sees it. -
The Research Gallery skips a bundle whose every profile is a widget or lacks the
webtarget —plugin-designs/doxa-maps-spa/doxa-maps-spa.vue:605:if (profiles.every(p => p.widget || (Array.isArray(p.targets) && !p.targets.includes('web')))) continueSo
"targets": ["desktop"]removes your plugin from the gallery. That is the feature, not a bug: it keeps a plugin that cannot work on a static host out of a surface that is served from one.bun run check:gallery-embedcovers the gallery's embeds.
The worked example is Rudder: its speech model runs in a local backend started by bun run dev
and run by Tauri as a sidecar, so "a website has no local engine" (its _comment). It is
correctly absent from the gallery and makes no network call on a static host.
- Say why in
_commentwhen you declare it — the next reader needs the reason, not just the list. - A
backendkey alongside it names the folder that must be running. Seebackend.md. - Do not add
targets"for documentation". Adding it withoutwebsilently removes you from the gallery.
Where a plugin actually runs
The three outputs, all from the same built folder:
| Output | What it is | How |
|---|---|---|
| Web folder | public/dx-maps/, embeddable by iframe or script tag from any host |
copy the folder; snippets in manifest.json |
| Mobile app | the same folder wrapped by Capacitor (capacitor.config.json webDir) |
bun run phone |
| Desktop app | the same folder in Tauri (deployment/src-tauri/), a proof of concept |
bun run tauri:build |
WordPress (deployment/wordpress/) embeds the web folder's snippets; it is experimental.
Embeddability is checked, unlike targets: bun run check:embed-targets runs
internal/check-embed-targets-exist.mjs.
Related
glossary.md— bundle, profile, preset, stage vs capabilitybackend.md— plugins that need a service, and the user tiers