The marketplace system ships as containers/libreportal-marketplace (a normal app definition, hidden behind CFG_DEV_MODE via a new CFG_<APP>_DEV_ONLY convention): it serves the signed catalog tree plus a client-rendered browse UI over the same index.json every box verifies. The official instance is our own install of this app; self-hosting a marketplace = installing it. Community submissions stay PR -> review -> sign (phase 2). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
7.7 KiB
LibrePortal Marketplace — the marketplace app (Roadmap / Design)
Status: direction decided 2026-07-03 (container MVP planned; submissions phase 2) ·
Audience: us, future-self · Scope: the browsable marketplace system — the
apps.nextcloud.com analogue — shipped as a LibrePortal app, and how community
submissions eventually flow through it · Builds on updates-and-distribution.md §3/§8.
1. The decided shape: the marketplace is a LibrePortal app
The marketplace server ships in this repo as containers/libreportal-marketplace/
— a normal app definition installed through the normal pipeline, with one twist:
it is dev-mode-only (see §4). The official marketplace.libreportal.org is
simply our instance of this app running on our own box — full dogfooding — and
anyone who wants their own marketplace enables Developer Mode and installs the
same app. "Open source and self-hostable" is therefore not a promise, it's the
literal distribution mechanism.
The marketplace has two user-facing surfaces over one source of truth:
┌────────────────────────────────────────────┐
│ THE SIGNED CATALOG (the source of truth) │
│ <base>/<channel>/index.json (+ .minisig) │
│ <base>/<channel>/payloads/<id>.tar.gz │
└───────────┬───────────────────┬────────────┘
│ │
boxes fetch + verify it the marketplace app
│ serves + renders it
┌──────────────▼─────────┐ ┌──────▼──────────────────┐
│ IN-APP (every box) │ │ libreportal-marketplace │
│ App Center grid shows │ │ (dev-mode app): hosts │
│ "Available — Add" │ │ the catalog tree + a │
│ cards; Add = task → │ │ browsable website: │
│ verify → drop-in │ │ browse, search, per-app │
└────────────────────────┘ │ pages, submit info │
└─────────────────────────┘
Boxes never talk to the website and never trust it — they only trust the minisign signature on the catalog. A marketplace instance can be compromised, defaced, or down and no install is ever at risk. That property is load-bearing; every choice below preserves it.
2. What the container serves
One app, one docroot, two things in it:
- The catalog tree itself —
/<channel>/index.json(.minisig)+/<channel>/payloads/…, exactly the layoutmake_app.sh/make_hotfix.shemit intodist/. A box pointed at this instance viaCFG_RELEASE_BASE_URLconsumes it directly. - The browse UI — static pages (no third-party assets, same rule as the
WebUI) rendered client-side from the same
index.json: front page by category, search, one page per app (title, description, icon, publisher + trust badge, version), a copyablelibreportal app add <slug>snippet, and a "run your own marketplace" page documenting exactly this app + theCFG_RELEASE_BASE_URLknob. Client-side rendering means the UI needs no build step and can never disagree with the catalog it serves.
No backend. No accounts, no database, no API server in the MVP. Publishing
to a marketplace = running the publisher tools and syncing their dist/
output into the app's docroot (its bind-mounted data dir).
Server: nginx:alpine static serving (the pattern the retired
getlibreportal container established: JSON no-cache, tarballs/sigs immutable).
Traefik routing, ports, backup labels — all standard app-definition machinery.
3. Community submissions (phase 2 — the F-Droid flow, not the Play flow)
When community apps open up (demand-gated, per updates-and-distribution.md
§8.5 fork 4), submission is a pull request, not an upload:
- Author PRs their app folder (
containers/<app>/shape — the drop-in contract indocs/contributing/development.md) to a public catalog repo. - CI validates mechanically: the bundle tarball contract, compose lint, no host-script hooks unless flagged for review, icon/meta present.
- A maintainer reviews — especially any
tools/*.sh(the one genuinely dangerous surface, §3.2) — then signs and publishes with the same tools used for first-party apps. The entry lands in the index withpublisher:<author>,trust:"community". - The marketplace site renders the submission queue from the PR list (links, not accounts) and the published result from the index.
Everything ends in "reviewed → signed → one public catalog" because the boxes only trust the signature. An account-based submission portal could be added to the marketplace app later, but it would only ever be a nicer way to open the PR — it can never become an unreviewed upload channel.
4. Dev-mode gating (the important part)
The marketplace app must not confuse regular users browsing their App Center —
running a marketplace is an operator/developer act. So it ships hidden unless
Developer Mode is on, riding the existing CFG_DEV_MODE machinery (10-click
logo easter egg; auto-enabled on git/local installs):
- New app-level convention:
CFG_<APP>_DEV_ONLY=truein the app's.config. webuiGenerateLibrePortalConfigemits it asdev_only: trueinapps.json.- The App Center filters
dev_onlyapps out of the grid/search unlesswindow.systemConfigs.CFG_DEV_MODE === 'true'(the same flag the**DEV**config-field filter uses — seefield-factory.js_filterDevKeys). - The CLI stays honest:
libreportal app install libreportal-marketplaceworks regardless (dev-only is decluttering, not a security boundary — the security boundary is the signing key, which the app never contains).
The convention is generic — any future operator-grade app (build servers, relay infrastructure) can reuse it.
5. Explicitly not doing
- No dynamic marketplace backend (accounts / uploads / ratings) in the MVP — re-affirming §3's decision; nothing in the in-app UX needs it, and phase 2 submissions stay review-and-sign whatever front door exists.
- No telemetry on the marketplace site (same rule as the product); download counts, if ever wanted, come from static server logs on our instance only.
- No website-only metadata. If the site needs a field (e.g. screenshots
later), it enters the catalog format as an optional envelope/
metaaddition so in-app and website stay one source of truth.
6. Sequencing
- Now: the in-app registry slice (
updates-and-distribution.md§8.7) — catalog format gainsmeta(category/description/icon),make_app.shpublishes app bundles. The catalog becomes real. - Now (after the publisher tool exists):
containers/libreportal-marketplace/MVP — nginx app serving the catalog tree + client-rendered browse UI, plus theCFG_<APP>_DEV_ONLYgating convention. End-to-end tests then run against a real marketplace instance instead ofpython3 -m http.server. - Demand-gated: the community submission flow (public catalog repo + CI validation + review/sign tooling), yellow-tier rendering, and the in-app community trust-tier UX (host-script quarantine — §8.7 deferred).