2026-08-25 16:27:57 +02:00
# 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.
2026-08-25 18:24:39 +02:00
### Elke image moet uit een register komen, en ` pull_policy` helpt niet
2026-08-25 18:13:57 +02:00
2026-08-25 18:24:39 +02:00
**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:
2026-08-25 18:13:57 +02:00
2026-08-25 18:24:39 +02:00
` ``
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.
2026-08-25 18:13:57 +02:00
2026-08-25 16:27:57 +02:00
**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