From 0afd1de819262b1a42e881c2eca4f02ab4f1203f Mon Sep 17 00:00:00 2001 From: librelad Date: Fri, 3 Jul 2026 20:30:07 +0100 Subject: [PATCH] =?UTF-8?q?docs(roadmap):=20marketplace=20app=20design=20?= =?UTF-8?q?=E2=80=94=20dev-mode=20container=20+=20catalog=20+=20submission?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The marketplace system ships as containers/libreportal-marketplace (a normal app definition, hidden behind CFG_DEV_MODE via a new CFG__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 Signed-off-by: librelad --- docs/roadmap/marketplace-website.md | 132 ++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 docs/roadmap/marketplace-website.md diff --git a/docs/roadmap/marketplace-website.md b/docs/roadmap/marketplace-website.md new file mode 100644 index 0000000..f2f4b21 --- /dev/null +++ b/docs/roadmap/marketplace-website.md @@ -0,0 +1,132 @@ +# 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) │ + │ //index.json (+ .minisig) │ + │ //payloads/.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** — `//index.json(.minisig)` + + `//payloads/…`, exactly the layout `make_app.sh` / `make_hotfix.sh` + emit into `dist/`. A box pointed at this instance via `CFG_RELEASE_BASE_URL` + consumes 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 copyable `libreportal app add ` snippet, and a + "run your own marketplace" page documenting exactly this app + the + `CFG_RELEASE_BASE_URL` knob. 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**: + +1. Author PRs their app folder (`containers//` shape — the drop-in contract + in `docs/contributing/development.md`) to a public catalog repo. +2. CI validates mechanically: the bundle tarball contract, compose lint, no + host-script hooks unless flagged for review, icon/meta present. +3. 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 with `publisher:`, + `trust:"community"`. +4. 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__DEV_ONLY=true` in the app's `.config`. +- `webuiGenerateLibrePortalConfig` emits it as `dev_only: true` in `apps.json`. +- The App Center filters `dev_only` apps out of the grid/search unless + `window.systemConfigs.CFG_DEV_MODE === 'true'` (the same flag the `**DEV**` + config-field filter uses — see `field-factory.js` `_filterDevKeys`). +- The CLI stays honest: `libreportal app install libreportal-marketplace` works + 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/`meta` addition + so in-app and website stay one source of truth. + +## 6. Sequencing + +1. **Now:** the in-app registry slice (`updates-and-distribution.md` §8.7) — + catalog format gains `meta` (category/description/icon), `make_app.sh` + publishes app bundles. The catalog becomes real. +2. **Now (after the publisher tool exists):** `containers/libreportal-marketplace/` + MVP — nginx app serving the catalog tree + client-rendered browse UI, plus + the `CFG__DEV_ONLY` gating convention. End-to-end tests then run against + a real marketplace instance instead of `python3 -m http.server`. +3. **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).