Plan Eigenimage, fase 1 tot en met 3. De vier templates verhuizen naar tools/electrum-gate/ zonder extensie; daarnaast Dockerfile (nginx:1.30-alpine plus python3), entrypoint.sh (het command-blok van de compose, zonder $$) en build.sh naar het voorbeeld van Evolu Relay. Een image voor beide containers, gebouwd op de Umbrel; open punt 2 en 3 daarmee beslist. Inhoudelijk anders dan alleen verplaatst: het log_format staat in stream.conf zelf, het backend-adres komt via twee plaatshouders zonder dollarteken uit de omgeving (ook in de server-service), en de pagina haalt versie en adres uit status.json via GATE_APP_VERSION. Tests mee verhuisd en uitgebreid: entrypoint.sh en Dockerfile in plaats van het command-blok, en de tag in de compose gelijk aan VERSION in build.sh voor elke eigen image. Mutatie-getest met drie ingrepen. Nog niet gebouwd: er is hier geen Docker. De tag staat ongepind tot de eerste push; dat is fase 4 en die is van de gebruiker. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
600 lines
31 KiB
Markdown
600 lines
31 KiB
Markdown
# Umbrel community app store: de spec waar deze repo aan moet voldoen
|
|
|
|
Naslag, geen plan. Dit is wat umbrelOS verwacht van een repo die als **community app store** dient, plus
|
|
het mechanisme waarmee een app van Electrum-backend kan wisselen. Uitgezocht op 18-08-2026 tegen de
|
|
bronnen die onderaan staan; elk feit hieronder komt uit code of een manifest in die repo's, niet uit een
|
|
blogpost.
|
|
|
|
> **Geldt voor beide apps in deze repo.** Dit document is op 18-08-2026 geschreven toen er één app was, en
|
|
> op sommige plekken is Electrum Gate nog het voorbeeld. De regels zelf zijn eigenschappen van umbrelOS en
|
|
> gelden onverkort voor Evolu Relay. De uitzondering is §5, "Wat deze repo nu níet heeft": dat is een
|
|
> momentopname van 18-08-2026 en gaat alleen over Electrum Gate. Wat er voor de relay ánders ligt, staat in
|
|
> het masterplan **Umbrelapp** §4.
|
|
|
|
## 1. De repo-vorm
|
|
|
|
Een community app store is een gewone GitHub-repo met deze vorm:
|
|
|
|
```
|
|
<repo-root>/
|
|
├── umbrel-app-store.yml # id + name van de store
|
|
├── <store-id>-<app-id>/ # één map per app
|
|
│ ├── umbrel-app.yml
|
|
│ └── docker-compose.yml
|
|
└── <store-id>-<andere-app>/
|
|
```
|
|
|
|
`umbrel-app-store.yml` heeft precies twee velden:
|
|
|
|
```yaml
|
|
id: whatsnext
|
|
name: WhatsNext?
|
|
```
|
|
|
|
Het `id` is een **verplichte prefix voor elk app-id in de store**. Een store met `id: whatsnext` die een
|
|
app `electrum-gate` bevat, heeft dus een map `whatsnext-electrum-gate/` en in het manifest
|
|
`id: whatsnext-electrum-gate`. Mapnaam en manifest-id moeten gelijk zijn.
|
|
|
|
Dat is meteen de reden dat het store-id niet naar één app genoemd moet worden: het zit in het id van
|
|
elke app die er ooit bij komt, en een app-id wijzigen is voor umbrelOS een andere app. Toen Evolu Relay
|
|
er op 25-08-2026 bij kwam, hoefde er daarom niets te hernoemen.
|
|
|
|
De gebruiker voegt de store toe door de URL van de repo in de umbrelOS-interface te plakken. Er is geen
|
|
review, geen submissie en geen wachttijd; dat is precies de reden om deze weg te kiezen.
|
|
|
|
### De repo hoeft niet op GitHub te staan
|
|
|
|
De documentatie van Umbrel praat consequent over GitHub, maar de code doet dat niet. In
|
|
`app-repository.ts` van umbreld is de enige controle op de URL deze:
|
|
|
|
```ts
|
|
function isValidUrl(url: string) {
|
|
try {
|
|
void new URL(url)
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
```
|
|
|
|
Geen hostnaamcontrole, geen `github.com`. Klonen gaat met **isomorphic-git**, een implementatie in
|
|
JavaScript:
|
|
|
|
```ts
|
|
await git.clone({
|
|
fs: fse,
|
|
http,
|
|
url: this.url,
|
|
dir: temporaryPath,
|
|
depth: 1,
|
|
singleBranch: true,
|
|
})
|
|
```
|
|
|
|
Een eigen Gitea of Forgejo kan dus de app store zijn. Daar zitten wel drie voorwaarden aan die uit deze
|
|
aanroep volgen en die niet in de documentatie staan:
|
|
|
|
1. **HTTPS, geen SSH.** isomorphic-git spreekt het smart-HTTP-protocol. Een `git@host:pad`-URL werkt niet;
|
|
het moet `https://host/gebruiker/repo.git` zijn.
|
|
2. **Anoniem kloonbaar.** Er wordt geen `onAuth` meegegeven, dus umbreld heeft geen manier om
|
|
inloggegevens aan te bieden. De repo moet publiek leesbaar zijn. Dat is meteen het antwoord op de
|
|
vraag of een privérepo kan: nee. Lukt het niet, dan meldt umbrelOS `HTTP Error: 401 Unauthorized` bij
|
|
het toevoegen van de store.
|
|
|
|
**Test dit niet met een gewone `git ls-remote`.** Op een machine die ooit naar die repo gepusht heeft,
|
|
levert een credential-helper de opgeslagen inloggegevens stilzwijgend aan en slaagt de test ten
|
|
onrechte. Ook met `GIT_TERMINAL_PROMPT=0`, want dat onderdrukt alleen de vráág om een wachtwoord. De
|
|
test die de toestand van umbreld nabootst:
|
|
|
|
```sh
|
|
GIT_TERMINAL_PROMPT=0 git -c credential.helper= ls-remote https://host/gebruiker/repo.git
|
|
```
|
|
|
|
Slaagt die, dan kan umbreld het ook. Faalt hij met `could not read Username`, dan vraagt de Git-server
|
|
om een aanmelding. Bij Gitea zijn daar **drie** onafhankelijke oorzaken voor, en ze moeten alle drie
|
|
goed staan:
|
|
|
|
1. **de zichtbaarheid van de repo**, onder Settings;
|
|
2. **`REQUIRE_SIGNIN_VIEW`** in de serverconfiguratie, die ook publieke repo's achter een aanmelding
|
|
zet;
|
|
3. **de zichtbaarheid van het account of de organisatie die eigenaar is**, in te stellen onder Site
|
|
Administration → User Accounts → Edit → Visibility.
|
|
|
|
Die derde is de valstrik, en hij kostte hier de meeste tijd. **Gitea staat niet toe dat een repo
|
|
zichtbaarder is dan zijn eigenaar.** Staat het account op "limited", dan wordt een repo die je op
|
|
public zet stilzwijgend teruggezet naar "intern", wat voor een niet-ingelogde bezoeker hetzelfde is
|
|
als privé. Er komt geen foutmelding; het enige spoor is dat het label na het opslaan op "intern"
|
|
blijft staan.
|
|
|
|
Handig om te weten bij het zoeken: de configuratie van het SynoCommunity-pakket voor Synology heet
|
|
**`conf.ini`** en niet `app.ini`. Het draaiende proces noemt het echte pad, en dat is sneller dan
|
|
zoeken:
|
|
|
|
```sh
|
|
ps -ef | grep -i "[g]itea"
|
|
```
|
|
3. **Een geldig TLS-certificaat.** Node valideert de keten. Een zelfondertekend certificaat op de
|
|
Git-server laat het klonen falen.
|
|
4. **SHA-1 als objectformaat, geen SHA-256.** isomorphic-git berekent object-id's uitsluitend met SHA-1.
|
|
In `src/utils/shasum.js` staat `import Hash from 'sha.js/sha1.js'` en verder
|
|
`crypto.subtle.digest('SHA-1', buffer)`; er is geen algoritmeparameter en geen tweede pad. Een repo die
|
|
met `--object-format=sha256` is aangemaakt, is voor umbreld dus onleesbaar.
|
|
|
|
Dit is het opschrijven waard omdat het pas laat zichtbaar wordt. Gitea biedt SHA-256 bij het aanmaken
|
|
van een repo gewoon als keuze aan, lokaal werkt alles, en de eerste `git push` vanaf een SHA-1-repo
|
|
faalt met `fatal: the receiving end does not support this repository's hash algorithm`, wat de
|
|
werkelijke oorzaak niet noemt. Achteraf omzetten kan niet met een instelling: dat is een nieuwe repo
|
|
plus `git fast-export` naar `git fast-import`. Gevonden op 18-08-2026, bij de eerste push van deze
|
|
repo.
|
|
|
|
Verder is `depth: 1, singleBranch: true` het vermelden waard: alleen de **standaardbranch** wordt
|
|
opgehaald. De store-inhoud moet daar staan, en een tag of tweede branch doet niets.
|
|
|
|
Waar de kloon terechtkomt volgt uit `cleanUrl()`: hostnaam tot de eerste punt, gebruiker en repo uit het
|
|
pad, plus de eerste acht tekens van de SHA-256 van de URL. Voor
|
|
`https://sc.kamenier-hamer.nl/sysop/UmbrelApps.git` wordt dat `sysop-umbrelapps-sc-<hash>`. Praktisch
|
|
gevolg: **de URL is de identiteit van de store.** Verander je hem, dan is het voor umbrelOS een andere
|
|
store en moet de gebruiker opnieuw toevoegen.
|
|
|
|
## 2. Het manifest
|
|
|
|
Het schema staat in `packages/umbreld/source/modules/apps/schema.ts` van umbrelOS. Runtime-validatie is
|
|
daar op dit moment **uitgeschakeld** (`AppManifestSchema.parse` staat uitgecommentarieerd, er wordt een
|
|
cast gedaan), dus een fout manifest geeft geen nette foutmelding maar vreemd gedrag. Reden te meer om het
|
|
schema hier op te schrijven.
|
|
|
|
Velden die het schema kent, met de type-eis:
|
|
|
|
| Veld | Type | Opmerking |
|
|
|-|-|-|
|
|
| `manifestVersion` | semver | `1` volstaat; `1.1` is nodig zodra je `hooks/` gebruikt |
|
|
| `id` | string | gelijk aan de mapnaam, met store-prefix |
|
|
| `name`, `tagline`, `category`, `version` | string | `version` is vrij tekst, geen semver-eis |
|
|
| `port` | integer | de **web-UI-poort** die umbrelOS voor de tegel gebruikt, niet je TCP-poort |
|
|
| `description`, `support` | string | verplicht in het schema |
|
|
| `website` | URL | moet een geldige URL zijn |
|
|
| `gallery` | lijst van strings | verplicht in het schema, mag naar externe URL's wijzen |
|
|
| `icon` | string | optioneel in het schema, maar zonder icoon geen herkenbare tegel |
|
|
| `dependencies` | lijst van strings | app-id's; zie §4 |
|
|
| `implements` | lijst van strings | welk app-id deze app kan **vervangen**; zie §4 |
|
|
| `developer`, `submitter`, `submission`, `repo` | string/URL | optioneel |
|
|
| `releaseNotes`, `path`, `defaultUsername`, `defaultPassword` | string | optioneel |
|
|
| `deterministicPassword`, `torOnly`, `optimizedForUmbrelHome`, `disabled` | boolean | optioneel |
|
|
| `installSize` | integer | in bytes |
|
|
| `widgets` | lijst | vorm nog niet vastgelegd in het schema |
|
|
| `backupIgnore` | lijst van strings | paden die niet in de back-up meegaan |
|
|
| `permissions`, `defaultShell` | | optioneel |
|
|
|
|
## 3. De compose
|
|
|
|
Twee dingen die de huidige opzet van deze repo níet doet.
|
|
|
|
**`app_proxy` in plaats van eigen poorten en netwerken.** umbrelOS genereert zelf een proxy-service; je
|
|
vult alleen in waar hij heen moet wijzen. De hostnaam is `<app-id>_<compose-service>_1`:
|
|
|
|
```yaml
|
|
services:
|
|
app_proxy:
|
|
environment:
|
|
# De vorm is: <app-id>_<docker-service-naam>_1
|
|
APP_HOST: electrumtls-electrum-tls_web_1
|
|
APP_PORT: 80
|
|
```
|
|
|
|
Een eigen `networks:`-blok op topniveau hoort er niet: de skill in `getumbrel/umbrel-apps` zegt letterlijk
|
|
dat je `networks: default:` per service alleen gebruikt wanneer je een geteste statische IP of alias nodig
|
|
hebt. Het `umbrel_main_network` handmatig als extern netwerk aanhaken is de oude 0.5-manier.
|
|
|
|
### Elke app krijgt een Tor hidden service, en dat is de tweede weg naar binnen
|
|
|
|
Vastgesteld op 25-08-2026 bij de eerste installatie van Evolu Relay, nadat de gebruiker vroeg of die app
|
|
kwaad kon. umbrelOS start per app een `<app-id>-tor_server-1`-container en publiceert de app als hidden
|
|
service. **Dat staat niet in de compose van de app en je ziet het dus niet als je die leest.** Controleren:
|
|
|
|
```sh
|
|
cat ~/umbrel/tor/data/app-<app-id>/hostname
|
|
```
|
|
|
|
Bestaat dat bestand, dan is er een `.onion`-adres en is de app vanaf het internet bereikbaar. Uit te zetten
|
|
per app in de umbrelOS-interface.
|
|
|
|
**Of dat erg is, hangt volledig af van `PROXY_AUTH_ADD`**, en dat maakt het een combinatie die je apart moet
|
|
beoordelen in plaats van per instelling:
|
|
|
|
| App | Inlog op de proxy | Wat de hidden service dan blootlegt |
|
|
|-|-|-|
|
|
| Electrum Gate | aan (standaard) | de inlogpagina van umbrelOS. Het dashboard zelf zit erachter. Zijn TLS-poort 50022 loopt niet via de proxy en zit dus niet op het onion-adres |
|
|
| Evolu Relay | **uit** (`"false"`, want Trezor Suite kan niet inloggen) | de relay zelf, zonder enige aanmelding. Alleen beschermd doordat het adres onraadbaar is |
|
|
|
|
De regel die daaruit volgt: **zet je `PROXY_AUTH_ADD` op `"false"`, beoordeel dan meteen de hidden service.**
|
|
Die twee samen maken van "alleen mijn thuisnetwerk" ongemerkt "het internet". Ook bij een app mét inlog is
|
|
uitzetten verdedigbaar, want dan staat er tenminste geen aanmeldscherm op het internet dat er niet hoefde
|
|
te staan.
|
|
|
|
**Images gepind op digest.** De regel uit dezelfde skill: pin elke image als
|
|
`registry/repo:versie-of-commit@sha256:<digest>`, houd tag en digest samen, en gebruik de **multi-arch
|
|
index-digest**, niet de architectuurspecifieke. Te controleren met:
|
|
|
|
```bash
|
|
docker buildx imagetools inspect <image>:<tag>
|
|
```
|
|
|
|
Zowel `linux/amd64` als `linux/arm64` moeten erin zitten. Niet toegestaan: `latest`, meebewegende
|
|
branch-tags, en een digest zonder tag.
|
|
|
|
Dit raakt deze app hard: hij draait nu op `alpine:latest` plus een `apk add stunnel` bij élke start. Dat
|
|
is niet reproduceerbaar, het faalt zonder internet, en het is per definitie niet te pinnen.
|
|
|
|
### Hoe bestanden bij de app terechtkomen, en waarom dat bij een update anders gaat
|
|
|
|
Dit is niet gedocumenteerd en het is de valkuil met de langste terugverdientijd. Uitgezocht op
|
|
18-08-2026 in `apps.ts` en het script `legacy-compat/app-script` van umbreld.
|
|
|
|
**Bij installatie wordt de hele app-map gekopieerd** naar `${APP_DATA_DIR}`:
|
|
|
|
```ts
|
|
await $`rsync --archive --verbose --exclude ".gitkeep" ${appTemplatePath}/. ${appDataDirectory}`
|
|
```
|
|
|
|
Alles wat in de app-map staat, komt dus mee: scripts, configuratie, een `web/`-map. Een compose die
|
|
`${APP_DATA_DIR}/entrypoint.sh` mount, werkt daardoor gewoon.
|
|
|
|
**Bij een update wordt alleen een whitelist opnieuw gekopieerd.** In `app-script` staat:
|
|
|
|
```sh
|
|
UPDATE_FILES_WHITELIST_PRE="docker-compose.yml *.template exports.sh torrc hooks"
|
|
UPDATE_FILES_WHITELIST_POST="umbrel-app.yml"
|
|
```
|
|
|
|
Alles daarbuiten wordt bij een update **niet** ververst. Een gewijzigde `entrypoint.sh` of
|
|
`web/index.html` bereikt een bestaande installatie dus nooit; de gebruiker ziet zijn oude versie en er is
|
|
geen foutmelding. Alleen een verwijdering en herinstallatie brengt het over.
|
|
|
|
### Elke image moet uit een register komen, en `pull_policy` helpt niet
|
|
|
|
**Op het apparaat vastgesteld op 25-08-2026**, bij de eerste installatiepoging van Evolu Relay met een
|
|
image die alleen lokaal gebouwd was. De installatie faalde met:
|
|
|
|
```
|
|
Error: (HTTP code 404) unexpected - pull access denied for whatsnext/evolu-relay,
|
|
repository does not exist or may require 'docker login'
|
|
at /opt/umbreld/node_modules/docker-modem/lib/modem.js:382:17
|
|
```
|
|
|
|
**Het beslissende detail is die stacktrace, niet de foutmelding.** De pull komt uit `docker-modem`, de
|
|
Docker-clientbibliotheek van umbreld zelf, en dus rechtstreeks van de Docker Engine API. Compose komt er
|
|
niet aan te pas. Dat is ook te zien aan de regels `Downloaded 40.5% of app` in de journal: umbreld haalt
|
|
de images op en rapporteert de voortgang zelf.
|
|
|
|
Twee gevolgen, en het tweede is een valkuil die een halve middag kost:
|
|
|
|
1. **Elke `image:` in de compose van een app moet anoniem uit een register te halen zijn.** Een tag die
|
|
alleen in de lokale Docker-opslag van het apparaat staat, werkt niet, ook al zou `docker compose up`
|
|
hem daar prima vinden. De installatie stopt vóór het starten.
|
|
2. **`pull_policy: never` verandert daar niets aan**, want dat is een sleutel van de Compose-specificatie
|
|
en umbreld leest de compose niet om te pullen; het leest alleen welke images erin staan. Dit is hier
|
|
geprobeerd en het faalde identiek.
|
|
|
|
Wat hier eerder stond, en waarom dat misleidde: in `app-script` staat `compose "${app}" pull` bij
|
|
`install`, `update` en `post-patch-update`, en `up --detach --build` bij het starten. Dat bestand is de
|
|
**legacy-compat**-laag; op umbrelOS 1.x is het niet het pad dat een installatie vanuit de interface loopt.
|
|
Lees dus niet uit `app-script` af wat umbreld doet zonder te controleren of die weg ook echt bewandeld
|
|
wordt.
|
|
|
|
Dat `--build` blijft trouwens ook zonder dit verhaal een slecht idee voor een app: een `Dockerfile` staat
|
|
niet in de update-whitelist hierboven, dus een nieuwe versie vraagt een deïnstallatie, en de installatie
|
|
hangt tijdens het bouwen. Een bouwrecept hoort in `tools/` in de repo, en het resultaat in een register.
|
|
|
|
**Gevolg voor het ontwerp.** Zet logica die je later nog wilt kunnen wijzigen op een van deze plekken:
|
|
|
|
1. **in `docker-compose.yml` zelf**, bijvoorbeeld als een inline `command:`-blok. De compose staat in de
|
|
whitelist;
|
|
2. **in de image**, als je er toch een bouwt;
|
|
3. **in een `*.template`-bestand.** Dit is de nette umbrel-manier en hij doet twee dingen tegelijk: het
|
|
bestand staat in de whitelist, én `template_app` verwerkt bij elke start elk
|
|
`${APP_DATA_DIR}/*.template` naar dezelfde naam zonder de extensie, met de omgevingsvariabelen
|
|
ingevuld. Een `entrypoint.sh.template` wordt dus `entrypoint.sh` mét `${APP_ELECTRS_NODE_IP}` er al in
|
|
ingevuld.
|
|
|
|
Wat je op grond hiervan **niet** moet doen: een los shellscript naast de compose zetten en aannemen dat
|
|
een `git push` het uitlevert.
|
|
|
|
### Wat er in `app-data` staat, en waarom deze app er anders uitziet dan andere
|
|
|
|
Nagetrokken op 20-08-2026, nadat de gebruiker opmerkte dat hij bij andere apps geen app-code in de
|
|
app-map ziet en dat die map daar eerder voor data lijkt te zijn. Dat klopt, en het verschil zit niet in
|
|
umbrelOS maar in wat een app zelf in zijn map zet.
|
|
|
|
**Voor élke app geldt dat de hele app-map naar `app-data` gekopieerd wordt.** Dus ook bij andere apps
|
|
staan `docker-compose.yml`, `umbrel-app.yml`, `exports.sh` en `hooks/` in
|
|
`~/umbrel/app-data/<app-id>/`. Wat daar bij hen níet staat is hun programmacode, want die zit in een
|
|
Docker-image uit een registry. Bij ons staat die er wel: de agent, de nginx-config en de pagina worden uit
|
|
`app-data` gemount, omdat deze app geen eigen image bouwt.
|
|
|
|
**Config-bestanden uit de app-map mounten is een bestaand patroon, ook bij first-party apps.** Uit
|
|
`electrs/docker-compose.yml`, letterlijk:
|
|
|
|
```yaml
|
|
- ${APP_DATA_DIR}/torrc:/etc/tor/torrc:ro
|
|
- "${APP_DATA_DIR}/data/electrs:/data"
|
|
```
|
|
|
|
Dat is precies de vorm die deze app ook gebruikt. Het bewijs dat het bedoeld is, staat in de whitelist
|
|
hieronder: `torrc` en `*.template` staan er met naam in, en `template_app` verwerkt bij elke start
|
|
`${app_data_dir}/*.template` met **`envsubst`**. Dat laatste is ook de reden achter de architectuurregel in
|
|
`CLAUDE.md`: `envsubst` vervangt élke `${NAAM}`, ook een die niet bestaat, en die wordt dan leeg.
|
|
|
|
**Waar deze app wél van de conventie afwijkt: de data.** Andere apps zetten hun persistente data onder een
|
|
submap, `${APP_DATA_DIR}/data/...`, zoals in de regels hierboven. Deze app gebruikt
|
|
`${APP_DATA_DIR}/runtime` en `${APP_DATA_DIR}/certs`, dus naast de code in plaats van eronder. Dat werkt,
|
|
maar het is niet de vorm die iemand verwacht die andere apps kent.
|
|
|
|
Wat de code in `app-data` verder betekent, en dat is de prijs die bewust betaald is:
|
|
|
|
- **wijzigen kan alleen via de whitelist.** Vandaar dat hier alles een `*.template` is;
|
|
- **het gaat mee in de back-up**, want `app-data` is wat umbrelOS bewaart. Voor de code is dat ruis en
|
|
voor `runtime/` ook; `certs/` hoort er juist wél in. Het manifest heeft een veld `backupIgnore` om dat
|
|
te sturen, en dat wordt hier nog niet gebruikt;
|
|
- **er is geen bouwstap en geen registry**, en geen `apk add` bij het starten. Dat was de reden om het zo
|
|
te doen, en die staat in het plan **Appstore**, fase 4.
|
|
|
|
Bronnen: [`electrs/docker-compose.yml`](https://raw.githubusercontent.com/getumbrel/umbrel-apps/master/electrs/docker-compose.yml),
|
|
[`mempool/docker-compose.yml`](https://raw.githubusercontent.com/getumbrel/umbrel-apps/master/mempool/docker-compose.yml),
|
|
[`app-script`](https://raw.githubusercontent.com/getumbrel/umbrel/master/packages/umbreld/source/modules/apps/legacy-compat/app-script).
|
|
|
|
### De inlevereisen van de officiële appstore
|
|
|
|
Opgehaald op 20-08-2026 toen de gebruiker zei dat hij de app uiteindelijk als standaard-app wil
|
|
publiceren. De eisen staan niet in de README van `umbrel-apps` maar in de skill-documentatie die daar naar
|
|
verwijst:
|
|
[`.claude/skills/umbrel-package-app/SKILL.md`](https://raw.githubusercontent.com/getumbrel/umbrel-apps/master/.claude/skills/umbrel-package-app/SKILL.md).
|
|
Wat daar staat en wat deze app ervan doet:
|
|
|
|
| Eis | Deze app |
|
|
|-|-|
|
|
| Elke image gepind als `repo:versie@sha256:<digest>`, met `linux/amd64` én `linux/arm64` in de manifest-lijst. Verboden: `latest`, meebewegende branch-tags, een digest zonder tag | **Half, sinds 0.1.0 (07-09-2026).** De app heeft een eigen image; de digest komt erbij bij de eerste push en dat kan alleen op de Umbrel. Wat nog ontbreekt is `linux/arm64`: `build.sh` bouwt standaard één architectuur, multi-arch zit erin als schakelaar en is nooit geprobeerd |
|
|
| Mapnaam gelijk aan het app-id, lowercase kebab-case | Klopt, maar het id heeft nu het store-voorvoegsel `whatsnext-`. Dat is een eis van een **community** store; officiële apps hebben een kaal id, dus dit wordt `electrum-gate` |
|
|
| Manifestvelden in een vaste volgorde: `manifestVersion`, `id`, `category`, `name`, `version`, `tagline`, `description`, `releaseNotes`, `developer`, `website`, `dependencies`, `repo`, `support`, `port`, `gallery`, `path`, en daarna de optionele | Klopt sinds 20-08-2026, met een toets erop. Wat de spec niet noemt (`icon`, `backupIgnore`) staat áchter die reeks, dus de kop is letterlijk goed |
|
|
| `gallery: []` bij een nieuw pakket; het store-team doet de plaatjes | Klopt al. Zie de paragraaf hieronder: de inhoud van dit veld moet bij inlevering leeg zijn, het veld zelf blijft staan |
|
|
| `icon` weglaten bij inlevering; iconen worden apart gehost | Nu wél gevuld, en dat moet ook zolang dit een eigen store is. Staat daarom als **laatste** regel van het manifest: bij inlevering is dat de enige die weg hoeft |
|
|
| `app_proxy` met alleen omgevingsvariabelen, geen eigen `ports:` | Klopt. De `ports:` van 50022 staat op de server-service, en dat is normaal voor een niet-web-poort |
|
|
| Umbrel-inlog aan laten staan; `PROXY_AUTH_WHITELIST` alleen smal en voor paden die geen cookie kunnen sturen | Klopt: er is geen whitelist, dus ook `/api/` zit achter de inlog. Dat is de onderbouwing onder het uploadpad |
|
|
| Alle gebruikersstaat, config, uploads en geheimen onder `${APP_DATA_DIR}/data/...`, met een `.gitkeep` per map die bij de eerste start moet bestaan | Klopt sinds 0.0.9 |
|
|
| Niet buiten `${APP_DATA_DIR}` schrijven | Klopt. Wel **lezen** buiten: `${UMBREL_ROOT}/app-data/zoraxy/...` staat alleen-lezen gemount, en dat is het meest ongebruikelijke aan deze app. Reken op een vraag daarover bij de review |
|
|
|
|
### Iconen en de drie promo-afbeeldingen
|
|
|
|
Uitgezocht op 20-08-2026, nadat de gebruiker opmerkte dat de afbeeldingen bovenaan een app in de store er
|
|
allemaal hetzelfde uitzien: een achtergrond met een plaatje van de app erop. Dat klopt, en de reden is dat
|
|
**het store-team ze zelf maakt**. Ze staan dan ook niet in de app-repo maar in
|
|
[`umbrel-apps-gallery`](https://github.com/getumbrel/umbrel-apps-gallery).
|
|
|
|
**In de officiële store zijn het kale bestandsnamen.** Uit `electrs/umbrel-app.yml`:
|
|
|
|
```yaml
|
|
gallery:
|
|
- 1.jpg
|
|
- 2.jpg
|
|
- 3.jpg
|
|
- 4.jpg
|
|
```
|
|
|
|
Wat er van een inzender gevraagd wordt, uit de inleverdraden: **1440 bij 900 pixels, PNG, drie tot vijf
|
|
stuks**, of drie tot vijf gewone schermafbeeldingen waarna het team de opmaak doet. Gecombineerd met de
|
|
regel uit de packaging-documentatie ("set `gallery: []` for new packages") is de praktische route dus:
|
|
schermafbeeldingen aanleveren, veld leeg laten. Het veld zelf blijft op zijn plek in de volgorde staan;
|
|
alleen de inhoud is leeg.
|
|
|
|
**In een eigen store zijn het absolute URL's**, net als het icoon. Uit het voorbeeld in de
|
|
community-store-template:
|
|
|
|
```yaml
|
|
icon: https://svgur.com/i/mvA.svg
|
|
gallery:
|
|
- https://i.imgur.com/yyVG0Jb.jpeg
|
|
- https://i.imgur.com/yyVG0Jb.jpeg
|
|
- https://i.imgur.com/yyVG0Jb.jpeg
|
|
```
|
|
|
|
Voor deze store kan dat dus met dezelfde truc als `icon` nu doet: de bestanden in de repo zetten en er met
|
|
hun raw-URL naar wijzen.
|
|
|
|
**Eén waarschuwing die niet over formaten gaat.** Een schermafbeelding van de certificaatkeuze op deze
|
|
machine toont veertien hostnamen van de gebruiker, en een van de verbindingsregels toont zijn domein. Voor
|
|
een publieke winkelpagina horen daar plaatsvervangende namen in. Dat is een andere afweging dan de
|
|
certificate-transparency-logs die in de app-beschrijving staan: die zijn per certificaat op te zoeken, een
|
|
winkelpagina zet de hele lijst bij elkaar.
|
|
|
|
Bronnen: [`electrs/umbrel-app.yml`](https://raw.githubusercontent.com/getumbrel/umbrel-apps/master/electrs/umbrel-app.yml),
|
|
[voorbeeld uit de community-store-template](https://raw.githubusercontent.com/getumbrel/umbrel-community-app-store/master/sparkles-hello-world/umbrel-app.yml),
|
|
[`umbrel-apps-gallery`](https://github.com/getumbrel/umbrel-apps-gallery).
|
|
|
|
### Hoe umbrelOS een update ziet
|
|
|
|
Via het veld **`version`** in `umbrel-app.yml`. umbreld haalt de repo periodiek op en vergelijkt de
|
|
versie in de store met die van het geïnstalleerde manifest in `${APP_DATA_DIR}/umbrel-app.yml`. Dat
|
|
verklaart ook waarom `umbrel-app.yml` als **laatste** wordt gekopieerd bij een update
|
|
(`UPDATE_FILES_WHITELIST_POST`): het geïnstalleerde manifest is de administratie van wat er staat, en dat
|
|
mag pas bijgewerkt worden als de rest binnen is.
|
|
|
|
**Hoe die vergelijking werkt, voor zover het uitmaakt: `0.0.10` na `0.0.9` levert een update op.** Op
|
|
20-08-2026 gecontroleerd op de Umbrel van de gebruiker. Dat sluit een tekstvergelijking met groter-dan uit,
|
|
want daarin is `0.0.10` kleiner dan `0.0.9`. Het is dus een gelijkheids- of semver-vergelijking, en over
|
|
tweecijferige versiedelen hoef je je geen zorgen te maken. Over een **omlaaggaand** nummer nog steeds wel:
|
|
dat blijft ongetoetst.
|
|
|
|
**Praktische regel die hieruit volgt: een wijziging zonder versieverhoging wordt nooit uitgerold.** Er
|
|
komt geen melding en geen fout; umbrelOS ziet simpelweg hetzelfde nummer en doet niets. Bij elke
|
|
functionele wijziging hoort dus een nieuwe `version`, ook bij een kleine reparatie in de compose of in
|
|
een template.
|
|
|
|
### Beschikbare omgevingsvariabelen
|
|
|
|
| Variabele | Betekenis |
|
|
|-|-|
|
|
| `APP_ID` | app-id uit manifest en mapnaam |
|
|
| `APP_VERSION` | het `version`-veld |
|
|
| `APP_DATA_DIR` | `${UMBREL_ROOT}/app-data/<app-id>` |
|
|
| `APP_MANIFEST_FILE` | pad naar het geïnstalleerde `umbrel-app.yml` |
|
|
| `UMBREL_ROOT` | de Umbrel-datawortel op de host |
|
|
| `DEVICE_HOSTNAME` | apparaatnaam zonder `.local` |
|
|
| `DEVICE_DOMAIN_NAME` | het `.local`-domein van het apparaat |
|
|
| `APP_DOMAIN` | het lokale `.local`-domein voor deze app |
|
|
| `APP_PROXY_HOSTNAME`, `APP_PROXY_PORT` | hostnaam en poort van de gegenereerde proxy |
|
|
| `NETWORK_IP` | basis-IP van het Umbrel-dockernetwerk, met `/16` voor het subnet |
|
|
| `TOR_PROXY_IP`, `TOR_PROXY_PORT` | de SOCKS-proxy van Umbrel |
|
|
| `TOR_DATA_DIR` | Tor-datamap op de host |
|
|
| `APP_HIDDEN_SERVICE` | het onion-adres van de app |
|
|
| `APP_SEED`, `APP_PASSWORD` | deterministisch per installatie afgeleide geheimen |
|
|
|
|
Daarnaast krijgt een app de exports van zijn **afhankelijkheden** (§4).
|
|
|
|
## 4. Wisselen tussen Electrs, Fulcrum en ElectrumX
|
|
|
|
Dit is het antwoord op de vraag "moet de gebruiker kunnen kiezen tussen Electrs en Fulcrum": umbrelOS
|
|
1.3 doet dat al, en een app hoeft er niets voor te bouwen.
|
|
|
|
**Bevestigd op de machine op 27-08-2026.** Alles hieronder was tot dan afgeleid uit de bron van umbreld en
|
|
uit de drie manifesten, en nooit uitgeprobeerd. Na de eerste volledige sync van Fulcrum heeft de gebruiker
|
|
in de umbrelOS-instellingen omgeschakeld van Electrs naar Fulcrum. Electrum Gate hoefde niet aangepast te
|
|
worden, en daarna kreeg een wallet van buiten over TLS gewoon data terug. Het aliassen van `NODE_IP` en
|
|
`NODE_PORT` werkt dus zoals hier beschreven. Wat het omschakelen zelf nog liet zien: umbrelOS herstart de
|
|
afnemende app erbij, en de app kwam terug met dezelfde certificaatkeuze.
|
|
|
|
**Het mechanisme.** Een app declareert een afhankelijkheid op een app-id, en een andere app kan zeggen
|
|
dat hij die rol vervult:
|
|
|
|
```yaml
|
|
# fulcrum/umbrel-app.yml en electrumx/umbrel-app.yml
|
|
implements:
|
|
- electrs
|
|
```
|
|
|
|
In `AppSettingsSchema` staat `dependencies: z.record(z.string())`: per app wordt bewaard welke
|
|
implementatie de gebruiker voor welke afhankelijkheid gekozen heeft. umbrelOS laadt vervolgens de
|
|
`exports.sh` van de **gekozen** app.
|
|
|
|
**Waarom dat werkt zonder aanpassing aan de afnemer.** De vervangers aliassen zichzelf naar de
|
|
Electrs-namen. Uit `fulcrum/exports.sh`:
|
|
|
|
```sh
|
|
export APP_FULCRUM_IP="10.21.22.200"
|
|
export APP_FULCRUM_NODE_IP="10.21.21.200"
|
|
export APP_FULCRUM_NODE_PORT="50002"
|
|
|
|
for var in IP NODE_IP NODE_PORT; do
|
|
electrs_var="APP_ELECTRS_${var}"
|
|
fulcrum_var="APP_FULCRUM_${var}"
|
|
...
|
|
export "$electrs_var"="${!electrs_var:=${!fulcrum_var}}"
|
|
done
|
|
```
|
|
|
|
`electrumx/exports.sh` doet exact hetzelfde met `APP_ELECTRUMX_*`. Ter vergelijking, `electrs/exports.sh`:
|
|
|
|
```sh
|
|
export APP_ELECTRS_IP="10.21.22.4"
|
|
export APP_ELECTRS_NODE_IP="10.21.21.10"
|
|
export APP_ELECTRS_NODE_PORT="50001"
|
|
```
|
|
|
|
**Wat dat voor deze app betekent, in twee regels:**
|
|
|
|
```yaml
|
|
dependencies:
|
|
- electrs
|
|
```
|
|
|
|
en in de compose `${APP_ELECTRS_NODE_IP}:${APP_ELECTRS_NODE_PORT}` gebruiken in plaats van een
|
|
hardgecodeerde containernaam. Daarmee werkt de app met alle drie de backends en kiest de gebruiker in de
|
|
umbrelOS-instellingen.
|
|
|
|
**Let op: alleen `IP`, `NODE_IP` en `NODE_PORT` worden gealiast.** Alles wat Electrs-specifiek is, zoals
|
|
`APP_ELECTRS_RPC_HIDDEN_SERVICE`, bestaat niet bij Fulcrum. Gebruik die dus niet.
|
|
|
|
### Het keuzedialoog bij de installatie, en wat er niet mee kan
|
|
|
|
Nagetrokken op 20-08-2026 in de bron, nadat de gebruiker meldde dat hij bij sommige apps twee dropdowns
|
|
krijgt en vroeg of "Zoraxy of Nginx Proxy Manager" ook zo kan. Dat dialoog bestaat, maar het werkt anders
|
|
dan het lijkt, en het verschil is precies wat de vraag beantwoordt.
|
|
|
|
**Eén dropdown per afhankelijkheid, niet per keuze.** In het schema van umbreld staat:
|
|
|
|
```ts
|
|
dependencies: z.array(z.string()).optional(),
|
|
implements: z.array(z.string()).optional(),
|
|
```
|
|
|
|
Een afhankelijkheid is dus één app-id en kan zelf géén lijst met alternatieven zijn; een geneste lijst
|
|
bestaat niet in dit schema. Wat er in zo'n dropdown staat, komt van de andere kant: elke app die
|
|
`implements: [<dat id>]` declareert, verschijnt erin. Twee dropdowns betekent dus twee afhankelijkheden.
|
|
Het voorbeeld dat de gebruiker zag is `mempool`:
|
|
|
|
```yaml
|
|
dependencies:
|
|
- bitcoin
|
|
- electrs
|
|
```
|
|
|
|
Dat geeft twee dropdowns, en de inhoud van de tweede is precies waar §4 hierboven over gaat: Electrs,
|
|
Fulcrum en ElectrumX declareren allemaal `implements: [electrs]`.
|
|
|
|
**Gevolg voor "Zoraxy of NPM": dat kan niet.** Nagekeken in beide manifesten in de officiële appstore:
|
|
`zoraxy` en `nginx-proxy-manager` declareren **geen** `implements`, dus er is geen gedeelde rol waarop een
|
|
afhankelijkheid kan wijzen. Er is ook geen weg omheen aan onze kant: `implements` staat in hún manifest en
|
|
niet in het onze. Wat wél kan is één van de twee hard eisen (`dependencies: [electrs, zoraxy]`), en dat is
|
|
dan een dropdown met één optie erin.
|
|
|
|
Bronnen: [`schema.ts`](https://raw.githubusercontent.com/getumbrel/umbrel/master/packages/umbreld/source/modules/apps/schema.ts),
|
|
[`mempool/umbrel-app.yml`](https://raw.githubusercontent.com/getumbrel/umbrel-apps/master/mempool/umbrel-app.yml),
|
|
[`zoraxy/umbrel-app.yml`](https://raw.githubusercontent.com/getumbrel/umbrel-apps/master/zoraxy/umbrel-app.yml),
|
|
[`nginx-proxy-manager/umbrel-app.yml`](https://raw.githubusercontent.com/getumbrel/umbrel-apps/master/nginx-proxy-manager/umbrel-app.yml).
|
|
|
|
### Poortbotsing: 50002
|
|
|
|
De poorten die de drie backends op de **host** publiceren:
|
|
|
|
| App | Host-poort |
|
|
|-|-|
|
|
| Electrs | 50001 |
|
|
| Fulcrum | 50002 |
|
|
| ElectrumX | 50001 intern, 50003 als publieke poort |
|
|
|
|
**Opgelost, en op 27-08-2026 in de praktijk bevestigd.** Hier stond dat deze app zelf 50002 publiceerde
|
|
voor stunnel, en dat de app daardoor met Fulcrum erbij niet zou starten. Dat gold voor de oude opstelling.
|
|
Open punt 1 van het plan **Configuratie** is op 19-08-2026 beslist op **50022**, en de handmatige
|
|
stunnel-container die 50002 vasthield is op 25-08-2026 weggehaald. Sinds de omschakeling draaien Fulcrum op
|
|
50002 en Electrum Gate op 50022 naast elkaar op dezelfde host.
|
|
|
|
## 5. Wat deze repo nu níet heeft
|
|
|
|
Puntsgewijs, zodat het plan **Appstore** hier direct op kan leunen:
|
|
|
|
1. geen `umbrel-app-store.yml`;
|
|
2. geen app-submap, alles ligt in de repo-root;
|
|
3. app-id `electrum-tls` mist de store-prefix;
|
|
4. geen `app_proxy`-service; wel handmatige host-poorten en een extern `umbrel_main_network`;
|
|
5. images op `latest`, stunnel via `apk add` bij elke start;
|
|
6. `dependencies: [electrs]` staat er wel, maar de compose gebruikt `${APP_ELECTRS_IP}` als host terwijl
|
|
`${APP_ELECTRS_NODE_IP}` bedoeld is: `APP_ELECTRS_IP` is de web-UI-container van Electrs, niet de
|
|
Electrum-server;
|
|
7. `icon` en `gallery` wijzen naar de assets van de Electrs-app in `getumbrel/umbrel-apps`, dus naar
|
|
andermans bestanden en naar een plaatje van een andere app;
|
|
8. `port: 50002` staat op de TCP-poort terwijl dat veld de web-UI-poort van de tegel is;
|
|
9. `submitter` en `submission` verwijzen naar `getumbrel/umbrel-apps`, wat voor een community store niet
|
|
klopt;
|
|
10. `install.sh` en `uninstall.sh` kopiëren naar `/home/umbrel/umbrel/apps/`, wat onder umbrelOS 1.x door
|
|
`umbreld` beheerd wordt.
|
|
|
|
## 6. Bronnen
|
|
|
|
Alles hierboven komt uit deze bestanden, geraadpleegd op 18-08-2026:
|
|
|
|
- `getumbrel/umbrel-community-app-store`, README en de voorbeeld-app `sparkles-hello-world/`
|
|
- `getumbrel/umbrel-apps`, `AGENTS.md` en `.claude/skills/umbrel-package-app/SKILL.md`
|
|
- `getumbrel/umbrel-apps`, de mappen `electrs/`, `fulcrum/` en `electrumx/`: manifest, compose en
|
|
`exports.sh`
|
|
- `getumbrel/umbrel`, `packages/umbreld/source/modules/apps/schema.ts` en `app-repository.ts`
|
|
- umbrelOS 1.3 release notes over swappable dependencies
|