Import the MetaMesh app family from Worph/AppStore

23 apps (MetaCore, the library/network/client tiers and 13 MetaFeeder*)
move out of the general-purpose Worph/AppStore into their own store so the
MetaMesh family releases on its own cadence.

- asset URLs repointed to cdn.jsdelivr.net/gh/Worph/MetaAppStore@main
- store scaffolding added (category/recommend/featured lists, licence,
  contributing, gitattributes)
- psd-source/meta-icons-v2 icon generator follows the apps

MetaMCP stays in Worph/AppStore: it is third-party metatool-ai software,
not part of MetaMesh.
This commit is contained in:
Worph
2026-09-13 16:18:29 +00:00
parent 998eba19f4
commit 39f82e1f96
145 changed files with 12964 additions and 1 deletions
+110
View File
@@ -0,0 +1,110 @@
# MetaGateway
Source-bridging tier for MetaMesh: ingests content from external sources and
makes it discoverable across the federation, so **MetaWatch** (and other
MetaShare peers) can find and stream it. Unifies the three dev gateway
sub-stacks (`docker-compose.yml` + `.torznab.yml` + `.tribler.yml`) into one
app sharing a single meta-core, ingester and enrichment plugin pair.
## What's in the stack (13 containers)
| Container | Role |
|-----------|------|
| `metagateway-app` | Gateway peer (feeders-only); writes resolved records to meta-core (dashboard behind OIDC) |
| `metagateway-core` | Dedicated meta-core; auto-store target + WebDAV poster store (watcher ON) |
| `metagateway-share` | meta-share **ingester**: announces resolved records on the swarm, seeds blobs |
| `metagateway-prowlarr` | Bundled Prowlarr torznab aggregator (own public route + forms login) |
| `metagateway-torznab-feeder` | torznab feeder → Prowlarr (+ librqbit BT fetch) |
| `metagateway-tribler` | Headless Tribler core (IPv8 overlay) |
| `metagateway-tribler-feeder` | tribler feeder → Tribler core |
| `book-feeder` / `paper-feeder` / `common-feeder` | Gutenberg / arXiv+PubMed / Wikimedia Commons |
| `metagateway-filename-parser` / `metagateway-tmdb` | Reused meta-sort enrichment plugins |
| `metagateway` | nginx-hash-lock OIDC perimeter for the dashboard |
The gateway discovers its feeders from `gateway-config.json` (the `feeders`
map), and `gateway-enabled.json` opts the `torznab` + `tribler` upstreams in.
Both are written by `pre-install-cmd` into `/DATA/AppData/metagateway/state/` —
edit them there and restart `metagateway-app` to change the feeder set.
## Self-contained meta-core
The gateway's meta-core is **private to the gateway** (it holds resolved feeder
records, not your media library), so this app does **not** use the shared
MetaCore app and coexists with MetaWatch + the standalone Meta* apps without
container-name or data-path collisions.
```
/DATA/AppData/metagateway/
├── metacore/ # gateway meta-core (shared by core + ingester)
├── files/ # WebDAV file store (tmdb posters/backdrops, seeded blobs)
├── state/ # gateway-config.json + gateway-enabled.json + redb caches
├── prowlarr/ # Prowlarr config + db
├── tribler/ # Tribler IPv8 identity + metadata db (+ installed config)
└── tmdb-cache/ # tmdb plugin response cache
```
## First-run setup
1. **Prowlarr** — open `https://metagateway-prowlarr-<domain>/`, finish the
wizard, add indexers, copy the **API Key**.
2. Set `PROWLARR_API_KEY` and `TMDB_TOKEN` (TMDB v4 read token) on this app,
then restart. Until set, the **torznab feeder reports as degraded** (the
reason is visible at its `/health` and in the dashboard) but never crashes —
the rest of the gateway stays healthy and the app boots normally. Optional:
`GIPHY_API_KEY` (common-feeder), `SCIHUB_MIRRORS` (paper-feeder).
3. **Tribler** needs no setup — the baked config installs on first boot.
## Federation
Two peers join the **public** libp2p swarm, on two different ports:
| Peer | Port | Role |
|------|------|------|
| `metagateway-app` | TCP **4002** | gateway **discovery** — publishes the kad provider record under the `metamesh-gateway` namespace; this is the peer a remote meta-search finds and dials for live fan-out |
| `metagateway-share` | TCP **4005** | the ingester's records + bitswap byte-serving (cohort `metamesh-share-default`) |
Tribler's IPv8 overlay is UDP **8092**.
**Cross-node discovery needs both the port open inbound AND a public address
set.** Without `META_GATEWAY_PUBLIC_ADDR` the gateway starts in local/mDNS-only
mode: it pulls in no kad bootstraps, publishes no provider record, and binds an
ephemeral port — so no off-host MetaWatch can ever find it, however open the
firewall is. The compose sets it from `${APP_PUBLIC_IPV4}`; on a PCS whose
public IP is not the v4 one, override it. Same-host MetaWatch discovery works
over mDNS on `pcs` regardless.
## Images
| Image | Tag |
|-------|-----|
| `ghcr.io/worph/meta-gateway` | `1.0.25` |
| `ghcr.io/worph/meta-share` | `1.0.26` |
| `ghcr.io/worph/meta-core` | `1.0.12` |
| `ghcr.io/yundera/appshield` | `2.0.9` |
No feeder images are listed here any more: every content source is a **separate
store app**, one per provider (MetaFeeder · Gutenberg / arXiv / Europe PMC /
Wikimedia Commons / Prowlarr / Tribler / …), each
pinning its own images. The gateway discovers them by URL over `pcs` and
soft-skips any that aren't installed.
| `lscr.io/linuxserver/prowlarr` | `latest` |
| `ghcr.io/yundera/nginx-hash-lock` | `1.0.7` |
**Tribler** uses the upstream `ghcr.io/tribler/tribler` image directly (there is
no custom `meta-tribler` image). `pre-install-cmd` fetches the complete REST
config (bound to `0.0.0.0:8085`, key `changeme`) to
`/DATA/AppData/metagateway/tribler-config/configuration.json` and bind-mounts it
at `/seed/configuration.json`; the entrypoint installs it into `/root/.Tribler`
on first boot.
## Resilience notes
- **Feeders run as `user: "0:0"`.** Their image bakes a non-root uid (10001) as
owner of the state dir, but the platform injects `user: 1000:1000` on services
without an explicit `user:`; running as root avoids a state-dir permission
crash.
- **A misconfigured feeder never crash-loops.** The feeder SDK keeps a
feeder whose config is bad/incomplete *running* and reports it as **degraded**
via `/health` (with the reason) rather than exiting — so one bad key can't take
the gateway down. The app gates torznab on `service_started`, not
`service_healthy`, for the same reason.
+580
View File
@@ -0,0 +1,580 @@
name: metagateway
# =============================================================================
# MetaGateway — bridges external sources into the MetaMesh network.
#
# A single gateway peer — the bridging tier ONLY. Every content source is now a
# SEPARATE store app (install whichever you want); the gateway discovers each by
# URL over `pcs`, stores its resolved records, and soft-skips any not installed:
# MetaFeeder · Gutenberg Project Gutenberg (+ Open Library cards)
# MetaFeeder · arXiv / Europe PMC academic papers (Sci-Hub: separate opt-in app)
# MetaFeeder · Wikimedia Commons open-licensed media (Giphy: separate opt-in app)
# MetaFeeder · Prowlarr Prowlarr indexers (video/torrent) — own enrichment
# MetaFeeder · Tribler Tribler IPv8 overlay (video/torrent) — own enrichment
# Plus a dedicated meta-core, and a co-located meta-share INGESTER that announces the
# gateway's resolved records on the swarm.
#
# Everything is scoped under the `metagateway-*` prefix and the per-stack
# `metagateway` network + /DATA/AppData/metagateway/ data root, so this stack
# installs alongside MetaWatch and the standalone Meta* apps with ZERO
# container-name / data-path collisions. Its meta-core is private to the
# gateway (holds resolved feeder records, not your library) — hence its own
# core rather than the shared MetaCore app.
#
# Federation: the gateway-app joins the PUBLIC IPFS DHT and is discovered for
# live fan-out on host port 4002; the meta-share ingester also joins the public
# swarm (records + bitswap byte-serving) on host port 4005 + mDNS on `pcs`, so
# MetaWatch peers find both the gateway and its seeded bytes.
# =============================================================================
services:
# ===========================================================================
# metagateway-core — dedicated meta-core. Auto-store target for every resolved
# feeder outcome; the gateway becomes a regular meta-core host of the CID.
# File watcher ON so tmdb-plugin posters PUT over WebDAV get indexed + seeded.
# ===========================================================================
metagateway-core:
# 1.0.1 carries the new meta-core API (records cids/ set, identity).
image: ghcr.io/worph/meta-core:1.0.12
container_name: metagateway-core
# Hard RAM cap, swap disabled (memswap_limit == mem_limit) → OOM
# instead of swap. Mirrors the dev sub-stack limits.
mem_limit: 768m
memswap_limit: 768m
hostname: metagateway-core
restart: unless-stopped
privileged: true
user: "0:0"
cpu_shares: 50
expose:
- 9000
volumes:
- ${DATA_ROOT:-/DATA}/AppData/metagateway/metacore:/meta-core:rw
- ${DATA_ROOT:-/DATA}/AppData/metagateway/files:/files:rw
environment:
- BASE_URL=http://metagateway-core:9000
- TZ=$TZ
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:9000/health"]
interval: 5s
timeout: 3s
retries: 30
start_period: 30s
# `pcs` so the external feeder apps can write records/posters to this core.
networks:
- metagateway
- pcs
# ===========================================================================
# metagateway-share — meta-share INGESTER role (META_CORE_URL set). Polls this
# gateway's meta-core, announces resolved records on Kamilata + rebuilds its
# Bloom filter, and seeds blobs over IPFS/bitswap so federated MetaWatch
# queries reach the gateway's records. Public libp2p on host port 4005
# (the gateway-app owns 4002 for discovery).
# ===========================================================================
metagateway-share:
# 1.0.8 = rsa-enabled libp2p (joins the public IPFS DHT — see metagateway-app)
# + P2P_ANNOUNCE support so remote peers can dial this bitswap seeder.
image: ghcr.io/worph/meta-share:1.0.26
container_name: metagateway-share
# Hard RAM cap, swap disabled (memswap_limit == mem_limit) → OOM
# instead of swap. Mirrors the dev sub-stack limits.
mem_limit: 3g
memswap_limit: 3g
hostname: metagateway-share
restart: unless-stopped
user: "0:0"
cpu_shares: 30
expose:
- 3000
# Own public libp2p port (4005). The gateway-app owns 4002 (discovery);
# this ingester's bitswap tier serves the gateway's seeded bytes to remote
# peers over the public DHT, so it needs its own dialable host port.
ports:
- "4005:4001"
volumes:
- ${DATA_ROOT:-/DATA}/AppData/metagateway/metacore:/meta-core:rw
# Operator settings (settings.json — NNTP creds + indexer API keys). This
# is the CREDENTIALED peer of the pair: it materialises NZB postings for
# credential-less clients. Losing settings.json on an update means the
# gateway silently stops serving nzb-release bytes until re-entered.
- ${DATA_ROOT:-/DATA}/AppData/metagateway/share-config:/config:rw
# Blockstore + seed manifest + NZB cache (META_SHARE_DATA=/data/meta-share).
# Without it the gateway's seeded corpus is wiped on every recreate.
- ${DATA_ROOT:-/DATA}/AppData/metagateway/share-data:/data:rw
environment:
P2P_LISTEN: "/ip4/0.0.0.0/tcp/4001"
# Advertise the routable address (published host port 4005) so kad
# provider records + identify carry a dialable multiaddr for remote
# bitswap fetches. Behind Docker NAT the swarm otherwise only sees
# container-internal addresses.
# ${APP_PUBLIC_IPV4}, NOT ${APP_PUBLIC_IP} — see the note on the
# gateway-app below: APP_PUBLIC_IP is the v6 address on an
# IPv6-primary PCS, and `/ip4/<v6>/tcp/...` fails to parse.
PUBLIC_ADDR: "/ip4/${APP_PUBLIC_IPV4}/tcp/4005"
# Public IPFS DHT bootstraps by explicit /dns4/.../tcp/4001 (the nested
# dnsaddr form resolves to wss-only in some container resolvers). rsa
# feature required to complete the noise handshake with these RSA-keyed
# (Qm...) bootstrap nodes.
# Includes mars.i.ipfs.io — the only surviving legacy *.i.ipfs.io
# bootstrapper (neptune/pluto/uranus/earth/saturn/venus are all NXDOMAIN).
# It is here for resilience: the four IPFS-Foundation bootstrappers can
# refuse an individual IP, and when they do a peer cannot join the DHT AT
# ALL — no provider record, no discovery, no gateway fan-out. Observed on
# watch.nsl.sh 2026-09-01: 2 connections with the four alone, 126 once a
# reachable fifth was added. Costs nothing when the four work.
KAD_BOOTSTRAP_PEERS: "/dns4/sg1.bootstrap.libp2p.io/tcp/4001/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt,/dns4/sv15.bootstrap.libp2p.io/tcp/4001/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN,/dns4/ny5.bootstrap.libp2p.io/tcp/4001/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa,/dns4/am6.bootstrap.libp2p.io/tcp/4001/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb,/dns4/mars.i.ipfs.io/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ"
META_CORE_URL: "http://metagateway-core:9000"
META_CORE_PATH: "/meta-core"
BASE_URL: "http://metagateway-share:3000"
# Renamed at the 1.0.26 bump (2026-08-26), as the old note here said to do:
# META_SHARE_PEER_API_URL was the pre-1.0.8 spelling and now only logs a
# deprecation warning. Do NOT set both.
INTERNAL_BASE_URL: "http://metagateway-share:3000"
TZ: "$TZ"
depends_on:
metagateway-core:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:3000/health"]
interval: 5s
timeout: 3s
retries: 10
start_period: 5s
networks:
- metagateway
- pcs
# ===========================================================================
# metagateway-app — the gateway peer (feeders-only). Discovers the feeder
# sidecars from /data/meta-gateway/gateway-config.json (written by
# pre-install-cmd), registers a RemoteFeederPlugin per upstream, and writes
# resolved records to its meta-core. Dashboard fronted by the hash-lock below.
# ===========================================================================
metagateway-app:
# 1.0.12 = rsa-enabled libp2p (required to join the public IPFS DHT: the
# bootstrap.libp2p.io nodes are RSA-keyed, so without `rsa` the noise
# handshake fails and the gateway never joins the DHT) + configurable
# P2P listen/announce so it's discoverable + dialable off-host + the
# 2026-07 discovery/filters fixes that make the meta-search fan-out
# actually route `fileType:video` queries here.
image: ghcr.io/worph/meta-gateway:1.0.25
container_name: metagateway-app
# Hard RAM cap, swap disabled (memswap_limit == mem_limit) → OOM
# instead of swap. Mirrors the dev sub-stack limits.
mem_limit: 1g
memswap_limit: 1g
hostname: metagateway-app
restart: unless-stopped
cpu_shares: 40
expose:
- 3000
# Public libp2p listener (4002). This is the peer MetaWatch/meta-search
# discovers via the kad `metamesh-gateway` provider namespace and dials
# for live gateway fan-out (torznab/tribler). Must be host-published +
# reachable inbound.
ports:
- "4002:4002"
volumes:
# gateway-config.json (feeders map) + gateway-enabled.json, written by
# pre-install-cmd. Runtime redb caches also live here.
- ${DATA_ROOT:-/DATA}/AppData/metagateway/state:/data/meta-gateway:rw
environment:
# NOTE: `meta_core_url` (and `meta_share_url`) live in gateway-config.json
# since v1.0.12 — no longer read from env. The pre-install-cmd below seeds
# them; the dashboard edits them at runtime. `META_GATEWAY_BASE_URL` stays
# in env because it's an identify-layer setting (baseUrl= token in the
# libp2p identify agent_version) — without it meta-search can't HTTP-fetch
# /api/gateway/plugins on this gateway to populate its routing table.
META_GATEWAY_PEER_ID: "metagateway"
# MUST be resolvable BY A REMOTE PEER — it is advertised verbatim in the
# identify `baseUrl=` token and used for nothing else in-process. A
# docker-internal name here (the old `http://metagateway-app:3000`) makes
# the gateway useless off-host even once kad discovery works: the remote
# capability fetch fails, the snapshot stays empty, and
# `upstreams_for_query_from_raw` then fans out to NOBODY. The
# `/api/gateway/plugins` carve-out in the Caddy labels below is what makes
# this URL answerable without a session.
META_GATEWAY_BASE_URL: "https://metagateway-${APP_DOMAIN}"
# Fixed libp2p port + advertise the routable address (published host
# port 4002) so provider records + identify carry a dialable multiaddr.
# Ceiling on established libp2p connections (new in meta-gateway 1.0.25).
# Joining the public DHT is otherwise UNBOUNDED: at 1.0.24 this peer
# climbed to ~600 connections and was OOM-killed by the memory limit
# below roughly once a minute, and because each boot minted a fresh
# peer-id, every provider record it published was stale within seconds —
# remote peers only ever got "Unexpected peer ID". 128 is the default;
# raise it only together with the memory limit.
META_GATEWAY_MAX_CONNECTIONS: "128"
META_GATEWAY_P2P_LISTEN: "/ip4/0.0.0.0/tcp/4002"
# Setting a PUBLIC_ADDR is what flips the gateway out of local/mDNS-only
# mode. Unset, meta-gateway pulls in NO kad bootstraps and sets
# kad_provide_enabled=false, so it never publishes its `metamesh-gateway`
# provider record and no off-host meta-search can discover it (it also
# binds an ephemeral /tcp/0, leaving the `ports` mapping below with
# nothing to forward to). See meta-gateway config.rs::resolve_network.
# ${APP_PUBLIC_IPV4}, NOT ${APP_PUBLIC_IP}: on an IPv6-primary PCS,
# APP_PUBLIC_IP holds the v6 address and `/ip4/<v6>/tcp/...` is not a
# parseable multiaddr — the gateway refuses to start rather than
# announce a lie. APP_PUBLIC_IPV4 is the v4 spelling, set on every box.
META_GATEWAY_PUBLIC_ADDR: "/ip4/${APP_PUBLIC_IPV4}/tcp/4002"
# Public IPFS DHT bootstraps via explicit /dns4/.../tcp/4001 (the nested
# dnsaddr default resolves to wss-only in some container resolvers).
# Includes mars.i.ipfs.io — the only surviving legacy *.i.ipfs.io
# bootstrapper (neptune/pluto/uranus/earth/saturn/venus are all NXDOMAIN).
# It is here for resilience: the four IPFS-Foundation bootstrappers can
# refuse an individual IP, and when they do a peer cannot join the DHT AT
# ALL — no provider record, no discovery, no gateway fan-out. Observed on
# watch.nsl.sh 2026-09-01: 2 connections with the four alone, 126 once a
# reachable fifth was added. Costs nothing when the four work.
KAD_BOOTSTRAP_PEERS: "/dns4/sg1.bootstrap.libp2p.io/tcp/4001/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt,/dns4/sv15.bootstrap.libp2p.io/tcp/4001/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN,/dns4/ny5.bootstrap.libp2p.io/tcp/4001/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa,/dns4/am6.bootstrap.libp2p.io/tcp/4001/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb,/dns4/mars.i.ipfs.io/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ"
TZ: "$TZ"
depends_on:
metagateway-core:
condition: service_healthy
# ALL feeders (book/paper/common + torznab/tribler) are SEPARATE store apps
# now — the gateway discovers them by URL from its feeders config and
# soft-skips any not installed, so they're not startup deps here.
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:3000/health"]
interval: 5s
timeout: 3s
retries: 10
start_period: 5s
# `pcs` so the gateway can reach the external feeder apps for discovery/query.
networks:
- metagateway
- pcs
# NOTE: ALL feeders are now SEPARATE store apps, one per provider — e.g.
# MetaFeeder · Gutenberg / arXiv / Europe PMC / Wikimedia Commons and the torrent
# ones (MetaFeeder · Prowlarr / Tribler). The gateway is purely the bridging tier:
# it discovers every feeder by URL (feeders config / dashboard) over `pcs`,
# stores their resolved records in its meta-core, and announces them on the
# swarm. Install whichever feeders you want; the gateway soft-skips any not
# reachable. No feeder sidecars are bundled here anymore.
# ===========================================================================
# OIDC perimeter for the MetaGateway dashboard. container_name MUST equal the
# URL prefix (`metagateway`) for the registrar's redirect-URI allowlist.
# ===========================================================================
metagateway:
image: ghcr.io/yundera/appshield:2.0.9
cpu_shares: 80
container_name: metagateway
hostname: metagateway
restart: unless-stopped
user: "0:0"
expose:
- 80
environment:
OIDC_REGISTRAR_URL: "http://auth-registrar:9092"
BACKEND_HOST: "metagateway-app"
BACKEND_PORT: "3000"
LISTEN_PORT: "80"
REDIRECT_HOST_SUFFIXES: "${APP_DOMAIN},${APP_PUBLIC_IP_DASH}.nip.io,${APP_PUBLIC_IP_DASH}.sslip.io"
depends_on:
- metagateway-app
labels:
# ---- metagateway-${APP_DOMAIN} (gateway TLS) ----
caddy_0: metagateway-${APP_DOMAIN}
caddy_0.import: gateway_tls
caddy_0.0_handle: "/health"
caddy_0.0_handle.reverse_proxy: "metagateway-app:3000"
# Capability contract for REMOTE peers that have no session.
# Exact-match path (no trailing `*`), so mutating routes like
# `PUT /api/gateway/plugins/:id` stay behind AppShield.
caddy_0.1_handle: "/api/gateway/plugins"
caddy_0.1_handle.reverse_proxy: "metagateway-app:3000"
caddy_0.2_handle: ""
caddy_0.2_handle.reverse_proxy: "{{upstreams 80}}"
# ---- metagateway-IP.nip.io (gateway TLS) ----
caddy_1: metagateway-${APP_PUBLIC_IP_DASH}.nip.io
caddy_1.import: gateway_tls
caddy_1.0_handle: "/health"
caddy_1.0_handle.reverse_proxy: "metagateway-app:3000"
# Capability contract for REMOTE peers that have no session.
# Exact-match path (no trailing `*`), so mutating routes like
# `PUT /api/gateway/plugins/:id` stay behind AppShield.
caddy_1.1_handle: "/api/gateway/plugins"
caddy_1.1_handle.reverse_proxy: "metagateway-app:3000"
caddy_1.2_handle: ""
caddy_1.2_handle.reverse_proxy: "{{upstreams 80}}"
# ---- metagateway-IP.sslip.io (Let's Encrypt) ----
caddy_2: metagateway-${APP_PUBLIC_IP_DASH}.sslip.io
caddy_2.0_handle: "/health"
caddy_2.0_handle.reverse_proxy: "metagateway-app:3000"
# Capability contract for REMOTE peers that have no session.
# Exact-match path (no trailing `*`), so mutating routes like
# `PUT /api/gateway/plugins/:id` stay behind AppShield.
caddy_2.1_handle: "/api/gateway/plugins"
caddy_2.1_handle.reverse_proxy: "metagateway-app:3000"
caddy_2.2_handle: ""
caddy_2.2_handle.reverse_proxy: "{{upstreams 80}}"
networks:
- metagateway
- pcs
# ===========================================================================
# AppShield gates — expose the bundled service UIs behind Yundera OIDC SSO so
# they're reachable + configurable from the dashboard's service links.
# AppShield's container_name MUST equal its subdomain prefix (the auth-registrar
# derives the OIDC client_id from it), so these use collision-free prefixes —
# `metagateway-core`/`-share` are already the backend names. These backends
# have no native auth, so they're reachable ONLY through these gates.
# metagatewaycore → meta-core dashboard (nginx :80, NOT the :9000 API)
# metagatewayshare → meta-share dashboard (:3000)
# Prowlarr + Tribler are SEPARATE store apps (their own UIs + auth); the
# feeders reach them over `pcs`, so there's no gate for them here.
# ===========================================================================
metagatewaycore:
image: ghcr.io/yundera/appshield:2.0.9
cpu_shares: 80
container_name: metagatewaycore
hostname: metagatewaycore
restart: unless-stopped
user: "0:0"
expose: ["80"]
environment:
OIDC_REGISTRAR_URL: "http://auth-registrar:9092"
REDIRECT_HOST_SUFFIXES: "${APP_DOMAIN},${APP_PUBLIC_IP_DASH}.nip.io,${APP_PUBLIC_IP_DASH}.sslip.io"
BACKEND_HOST: "metagateway-core"
BACKEND_PORT: "80"
LISTEN_PORT: "80"
labels:
caddy_0: metagatewaycore-${APP_DOMAIN}
caddy_0.import: gateway_tls
caddy_0.reverse_proxy: "{{upstreams 80}}"
caddy_1: metagatewaycore-${APP_PUBLIC_IP_DASH}.nip.io
caddy_1.import: gateway_tls
caddy_1.reverse_proxy: "{{upstreams 80}}"
caddy_2: metagatewaycore-${APP_PUBLIC_IP_DASH}.sslip.io
caddy_2.reverse_proxy: "{{upstreams 80}}"
depends_on:
- metagateway-core
networks:
- metagateway
- pcs
metagatewayshare:
image: ghcr.io/yundera/appshield:2.0.9
cpu_shares: 80
container_name: metagatewayshare
hostname: metagatewayshare
restart: unless-stopped
user: "0:0"
expose: ["80"]
environment:
OIDC_REGISTRAR_URL: "http://auth-registrar:9092"
REDIRECT_HOST_SUFFIXES: "${APP_DOMAIN},${APP_PUBLIC_IP_DASH}.nip.io,${APP_PUBLIC_IP_DASH}.sslip.io"
BACKEND_HOST: "metagateway-share"
BACKEND_PORT: "3000"
LISTEN_PORT: "80"
labels:
caddy_0: metagatewayshare-${APP_DOMAIN}
caddy_0.import: gateway_tls
caddy_0.reverse_proxy: "{{upstreams 80}}"
caddy_1: metagatewayshare-${APP_PUBLIC_IP_DASH}.nip.io
caddy_1.import: gateway_tls
caddy_1.reverse_proxy: "{{upstreams 80}}"
caddy_2: metagatewayshare-${APP_PUBLIC_IP_DASH}.sslip.io
caddy_2.reverse_proxy: "{{upstreams 80}}"
depends_on:
- metagateway-share
networks:
- metagateway
- pcs
networks:
metagateway:
name: metagateway
driver: bridge
pcs:
name: ${APP_NET:-pcs}
external: true
x-casaos:
architectures:
- amd64
- arm64
author: Worph Team
category: Media
# `needs-public-ip` is the reserved functional tag: this app publishes a raw
# libp2p TCP port that remote peers must dial inbound. Behind CGNAT or the
# mesh-router tunnel it still installs and starts, but peers cannot reach it.
tags: ["media", "needs-public-ip"]
developer: Aptero Team
description:
en_us: |
**The source-bridging tier for the MetaMesh network.**
MetaGateway ingests content from external sources and makes it
discoverable across the MetaMesh federation — so MetaWatch (and other
MetaShare peers) can find and stream it. It's the **bridging tier only**:
a dedicated meta-core that stores resolved records, plus a co-located
meta-share ingester that announces them on the swarm. Every content source
is a **separate MetaFeeder app**, one per provider (Prowlarr, Tribler, Usenet,
Gutenberg, arXiv, Europe PMC, Wikimedia Commons, …)
— install whichever you want; the gateway discovers each over the shared
network and soft-skips any not installed.
**Sources (each a separate store app):**
- **Prowlarr** - Torznab/Newznab indexers via the separate Prowlarr app (video/torrent/usenet)
- **Tribler** - Decentralized Tribler discovery (video/torrent)
- **Project Gutenberg** - Public-domain books (+ Open Library work cards)
- **arXiv / Europe PMC** - Academic papers (Sci-Hub: separate opt-in app)
- **Wikimedia Commons** - Open-licensed images (Giphy: separate opt-in app)
**How it fits:** MetaGateway is the *producer*; **MetaWatch** is the
*consumer*. Install MetaGateway to populate the network, then browse it in
MetaWatch. Self-contained — its meta-core is private to the gateway, so it
coexists with MetaWatch and the standalone Meta* apps without conflicts.
**Enrichment:** Each resolved release is enriched with filename parsing and
TMDB metadata (canonical content-addressed posters + plot) so it surfaces
cleanly in MetaWatch's poster-gated wall.
fr_fr: |
**La couche de passerelle de sources pour le réseau MetaMesh.**
MetaGateway ingère du contenu depuis des sources externes et le rend
découvrable à travers la fédération MetaMesh — pour que MetaWatch (et les
autres pairs MetaShare) puissent le trouver et le lire. Elle exécute tout
le lot de feeders en sidecars, plus un Prowlarr intégré, un cœur Tribler
headless, les plugins d'enrichissement meta-sort, un meta-core dédié et un
ingesteur meta-share co-localisé qui annonce les enregistrements résolus.
**Sources (chacune une app du store) :**
- **Prowlarr** - Indexeurs Torznab/Newznab via l'app Prowlarr (vidéo/torrent/usenet)
- **Tribler** - Découverte décentralisée Tribler (vidéo/torrent)
- **Project Gutenberg** - Livres du domaine public (+ fiches Open Library)
- **arXiv / Europe PMC** - Articles académiques (Sci-Hub : app optionnelle séparée)
- **Wikimedia Commons** - Images sous licence libre (Giphy : app optionnelle séparée)
**Place dans l'écosystème :** MetaGateway est le *producteur* ; **MetaWatch**
est le *consommateur*. Installez MetaGateway pour alimenter le réseau, puis
parcourez-le dans MetaWatch. Autonome — son meta-core est privé, donc il
cohabite avec MetaWatch et les apps Meta* sans conflit.
tagline:
en_us: Bridge torrent/usenet, books, papers and images into the MetaMesh network
fr_fr: Connectez torrent/usenet, livres, articles et images au réseau MetaMesh
title:
en_us: MetaGateway
icon: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaGateway/icon.png
screenshot_link:
- https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaGateway/screenshot-1.png
- https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaGateway/screenshot-2.png
- https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaGateway/screenshot-3.png
thumbnail: https://cdn.jsdelivr.net/gh/Worph/MetaAppStore@main/Apps/MetaGateway/thumbnail.png
index: /
store_app_id: metagateway
main: metagateway
is_uncontrolled: false
tips:
before_install:
en_us: |
**Self-contained — no MetaCore required.** MetaGateway ships its own
meta-core, ingester and feeders. Pair it with **MetaWatch** to browse
what it surfaces.
**Authentication:** The gateway dashboard is protected by Yundera's
Authelia via OIDC.
**The gateway works out of the box.** Books, papers and common-media
feeders need no setup. Torrent/video needs the separate **Prowlarr** and
**Tribler** apps — until they're installed + configured, those feeders
simply show as **"degraded"** in the dashboard (they don't crash the app).
**Torznab / Prowlarr setup (required for torrent video):**
1. Install the **Prowlarr** app from the store (it runs on the shared
network as `prowlarr`).
2. Open Prowlarr, complete its first-run wizard, then Indexers → Add
Indexer → add one or more public indexers (confirm they test green).
3. Settings → General → copy the **API Key**.
4. In the gateway dashboard, open the **torznab** plugin → configure, paste
the key into the indexer's API-key field, and Save. The torznab feeder
auto-reloads and turns healthy.
**Tribler (optional, decentralized torrents):** install the **Tribler**
app from the store; the gateway's tribler feeder connects to it
automatically over the shared network — no key needed.
**TMDB enrichment:** set the TMDB v4 token in the torznab / tribler plugin
config (dashboard) for canonical posters + overviews.
**Federation ports (optional).** The gateway-app host-publishes its
libp2p listener on TCP **4002** (discovery / live fan-out) and the
meta-share ingester on TCP **4005** (records + bitswap byte-serving) —
open both inbound for best cross-node reach; same-host discovery (a
MetaWatch on the same box) works over mDNS without them.
pre-install-cmd: |
# Feeders map: all feeders are now SEPARATE store apps, listed by their pcs
# URLs (MetaFeeder · Gutenberg / arXiv / Europe PMC / Wikimedia Commons /
# Prowlarr / Tribler — the opt-in Sci-Hub and Giphy apps are not seeded). The
# gateway soft-skips any feeder that isn't installed/reachable, and you can
# add/remove feeders at runtime from the dashboard. Each key/URL MUST match
# the corresponding app's feeder container hostname.
# Guarded: this hook reruns on every reinstall and every version upgrade, and
# the feeder map is editable from the dashboard — an unguarded `cat >` would
# silently discard the operator's feeders each time.
if [ ! -f /DATA/AppData/metagateway/state/gateway-config.json ]; then
cat > /DATA/AppData/metagateway/state/gateway-config.json <<'JSON'
{
"meta_core_url": "http://metagateway-core:9000",
"meta_share_url": "http://metagateway-share:3000",
"feeders": {
"gutenberg-feeder": { "url": "http://metafeedergutenberg-feeder:8080" },
"arxiv-feeder": { "url": "http://metafeederarxiv-feeder:8080" },
"europepmc-feeder": { "url": "http://metafeedereuropepmc-feeder:8080" },
"wikicommons-feeder": { "url": "http://metafeederwikicommons-feeder:8080" },
"prowlarr-feeder": { "url": "http://metafeederprowlarr-feeder:8080" },
"tribler-feeder": { "url": "http://metafeedertribler-feeder:8080" }
}
}
JSON
fi
if [ ! -f /DATA/AppData/metagateway/state/gateway-enabled.json ]; then
cat > /DATA/AppData/metagateway/state/gateway-enabled.json <<'JSON'
{
"disabled": [],
"enabled_opt_in": ["torznab", "tribler"]
}
JSON
fi
# Maison (Yundera dashboard) configuration. Read in preference to x-casaos.
# schema_version 2 is required for `folders`: an older Maison refuses the app
# outright rather than starting it without its directories.
x-compose-app:
schema_version: 2
# Created and chowned by Maison before any image is pulled, before any hook
# runs and before the containers start — on the first boot and every one after.
# Compose would otherwise create a missing bind source as a root-owned dir.
folders:
- path: /DATA/AppData/metagateway/metacore
user: $PUID
group: $PGID
- path: /DATA/AppData/metagateway/files
user: $PUID
group: $PGID
- path: /DATA/AppData/metagateway/state
user: $PUID
group: $PGID
- path: /DATA/AppData/metagateway/share-config
user: $PUID
group: $PGID
- path: /DATA/AppData/metagateway/share-data
user: $PUID
group: $PGID
# Same string as the caddy_0 label, so the tile link and the route agree.
webui-host: metagateway-${domain}
webui-path: "/"
Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

+64
View File
@@ -0,0 +1,64 @@
# MetaGateway — Rationale
## What deviation / exception is being requested
1. `metagateway-core` and `metagateway-share` run `user: 0:0`, and `metagateway-core` runs
`privileged: true`.
2. The stack publishes two host ports: `4002/tcp` (gateway discovery) and `4005/tcp`
(meta-share ingester bitswap).
3. The app exposes **three** public hostnames — `metagateway-`, `metagatewaycore-` and
`metagatewayshare-` — each with its own AppShield sidecar, so two sidecar
`container_name`s differ from the compose top-level `name:`.
## Why it is necessary
1. `metagateway-core` is a bundled MetaCore: it runs rclone remote mounts, which need a
mount namespace (`SYS_ADMIN` + `/dev/fuse` + an unconfined AppArmor profile), plus nginx
and Redis. `metagateway-share` writes its service registration into the root-owned
`/meta-core/services/`.
2. libp2p is **not HTTP** — Caddy cannot proxy it, and remote peers resolve this node's
public multiaddr from the kad-DHT and dial it directly. Without inbound reachability the
node makes outbound connections only and never serves bytes, which is the app's function.
The app therefore carries the reserved `needs-public-ip` tag.
3. Each hostname is a distinct UI (gateway dashboard, core dashboard, share dashboard) and
`auth-registrar` derives an app's OIDC `client_id` from the sidecar's container name via
PTR lookup. Three routes need three stable, distinct names; only one of them can equal
the top-level `name:`. `x-casaos.main` points at `metagateway`, the sidecar carrying the
primary route.
## Security mitigations in place
- Every publicly-routed hostname terminates at its own AppShield sidecar
(`ghcr.io/yundera/appshield:2.0.9`) behind the PCS's Authelia SSO. Each sidecar sets
`hostname:` equal to its `container_name` so `auth-registrar` can attest it via PTR and
accept its redirect URIs; the names are app-prefixed, so they cannot collide with another
app's services in the shared `pcs` DNS namespace.
- The backends carry no Caddy labels and publish **no HTTP port** to the host. The only
published ports are raw libp2p TCP.
- libp2p transport is authenticated and encrypted (Noise) and peer identity is a public
key, so an unauthenticated dial cannot read anything the node has not chosen to publish
to the swarm.
- No user directories are mounted — no `/DATA/Documents`, `/DATA/Downloads`, `/DATA/Media`
or `/DATA/Gallery`.
- `cpu_shares` on every service; memory-heavy services carry a hard `mem_limit` with
`memswap_limit` equal to it. All image tags pinned.
## Alternatives considered and rejected
- **Proxy libp2p through Caddy.** Not possible; the DHT advertises a raw TCP multiaddr.
- **One sidecar with path-based routing to all three UIs.** The three backends generate
absolute URLs and set cookies on their own hostnames; path-mounting them breaks both.
- **Drop `privileged` from `metagateway-core` for a `cap_add` list.** The reduced form is
most of what `privileged` grants and still failed to bring up the supervised daemons —
see `Apps/MetaCore/rationale.md`, which is the same container.
- **Split into separate store apps.** That is exactly what MetaCore / MetaShare /
MetaGatewayShared are. This app is the self-contained variant, for a box that wants the
gateway without the rest of the ecosystem installed.
## Data protection
All state lives under `/DATA/AppData/metagateway/`, declared in `x-compose-app.folders` and
archived by Maison on uninstall. The two operator-editable config seeds
(`gateway-config.json`, `gateway-enabled.json`) are written only when absent, so a reinstall
or version upgrade preserves the feeder map the user configured from the dashboard. Only
content published to the swarm leaves the box.
Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB