LibrePortal/docs/roadmap/marketplace-website.md
librelad 0afd1de819 docs(roadmap): marketplace app design — dev-mode container + catalog + submissions
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>
2026-07-03 20:30:07 +01:00

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 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 <slug> 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/<app>/ 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:<author>, 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_<APP>_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_<APP>_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).