# 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).