0014 — Nextcloud marketplace distribution via AppAPI ExApp
Status: Accepted, 2026-06-15. Resolves
openspec/changes/propose-nextcloud-marketplace-app (direction 4 of
the four-direction Nextcloud integration). Chosen over the proposal's
options A/B/C.
Context. Direction 4 asked whether Wanderer should be installable
from the Nextcloud app marketplace. The proposal framed three
architectures on the premise that Nextcloud apps are PHP: A (PHP
shim + Go sidecar), B (PHP reimplementation), C (Go→WASM embedded in
PHP). All three were unattractive — A leaks "install two services", B
rewrites everything in the wrong language, C depends on a non-standard
ext-wasm runtime — so the proposal recommended deferring.
That premise is obsolete. Nextcloud's AppAPI runs ExApps (External Apps) as Docker containers, managed by a Deploy Daemon, language-agnostic, and distributable through the App Store. AppAPI is a default dependency since Nextcloud 30.0.1.
Decision. Distribute Wanderer as an AppAPI ExApp — call it architecture D. Wanderer is already a single pure-Go binary, so it containerises trivially. A thin Go shim implements the AppAPI lifecycle the Deploy Daemon expects and fronts the existing Wanderer HTTP API:
GET /heartbeat(unauthenticated) — liveness; reflects whether the colocated Wanderer process is actually reachable.POST /init,PUT /enabled?enabled=(AppAPIAuth) — lifecycle hooks.- everything else (AppAPIAuth) — reverse-proxied to
wanderer serveon the container loopback. - Incoming auth is the
AUTHORIZATION-APP-APIshared secret (base64userid:secret) the Deploy Daemon injects asAPP_SECRET.
Two repos, one-way dependency. The ExApp surface ships as a
separate downstream repo, MWest2020/wanderer-exapp, NOT in this
core repo. This honours the marketplace change's spec requirement:
the core Go module gains no PHP/Composer/Nextcloud-app dependency and
go test ./... stays toolchain-free. The downstream repo consumes the
core by go install-ing a pinned version of cmd/wanderer into its
image; a release-triggered GitHub Action (repository_dispatch) bumps
the pin so the ExApp tracks core releases. The dependency runs one way
— exactly like the Billbird ← billbird-client ← Gitsweeper split.
Why a separate repo rather than a subdirectory: the core's API lives
under internal/ (module-private, not importable by another module),
so there is no clean in-repo import path anyway; and the ExApp has its
own release cadence and marketplace quality bar. A pinned-binary
dependency is the honest coupling.
Consequences.
- Marketplace distribution is feasible at far lower cost than the ~2-week PHP-shim estimate for option A — the shim is a few hundred lines of Go and the core needs zero changes.
- The "operators see Wanderer in Nextcloud" goal is now covered at three levels: as-target (scan a Nextcloud), as-output (publish scans into Nextcloud, ADR-adjacent / exporters spec), and — with this — installable inside Nextcloud.
- A core release must trigger the downstream rebuild for the ExApp to
stay current; that wiring (a dispatch step in the core's release
workflow) is a follow-up once the core tags releases. Until then the
ExApp pins
WANDERER_VERSION=main. - Validated end-to-end (2026-06-15). The ExApp was smoke-tested
against a live Nextcloud 30.0.17 + AppAPI 4.0.6: the image builds
(buildx), the container runs the shim + the pinned core binary, and
AppAPI registered it via a
manual-installdaemon — authenticating with the realAUTHORIZATION-APP-APIsecret, calling/init, and ending withapp_api:app:listshowingwanderer … [enabled]. The runtime contract (auth, lifecycle, proxy) is proven. - The ExApp
info.xmlmanifest is still a template — the live validation registered the app via--json-info(the runtime path), notinfo.xml(the App-Store packaging path). The AppAPI manifest schema drifted across versions (3.0+ deprecated<scopes>/<system>) and must be validated against the target AppAPI version before the app is published to the store.
Alternatives considered.
- A — PHP shim + Go sidecar: rejected. AppAPI's Deploy Daemon already manages the container, so the "two services" problem D was meant to avoid simply doesn't arise; a hand-written PHP shim adds a second language and release cadence for no gain over D.
- B — PHP reimplementation: rejected outright (rewrites every probe and rule in the wrong language; test coverage from zero).
- C — Go→WASM in PHP: rejected.
ext-wasmis non-standard and the probe networking surface (raw sockets, TLS) isn't exposed by Nextcloud's PHP/WASI runtime. - Subdirectory in the core repo: rejected —
internal/blocks in-module import, and it would risk dragging container/manifest concerns into the core's CI. A separate repo keeps the boundary crisp.
See also.
openspec/changes/.../propose-nextcloud-marketplace-app— the decision-space proposal this ADR resolvesMWest2020/wanderer-exapp— the downstream ExApp repo (the spike)- ADR-0013 (Nextcloud as OIDC) — sibling Nextcloud-integration decision
- Nextcloud AppAPI / ExApp docs: https://docs.nextcloud.com/server/latest/admin_manual/exapps_management/AppAPIAndExternalApps.html