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>
133 lines
7.7 KiB
Markdown
133 lines
7.7 KiB
Markdown
# 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).
|