529 lines
27 KiB
Markdown
529 lines
27 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.
|
||
|
|
|
||
|
|
**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.
|
||
|
|
|
||
|
|
**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 | **Nog niet.** `python:3-alpine` en `nginx:alpine` staan er kaal in. Dit is de grootste openstaande eis en hij kan alleen op de Umbrel zelf, met `docker buildx imagetools inspect` |
|
||
|
|
| 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.
|
||
|
|
|
||
|
|
**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 |
|
||
|
|
|
||
|
|
Deze app publiceert nu zelf 50002 voor stunnel. Met Fulcrum geïnstalleerd botst dat en start de app niet.
|
||
|
|
De keuze hierover staat als open punt in het plan **Configuratie**.
|
||
|
|
|
||
|
|
## 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
|