Bind an app to an alternate source
Step-by-step guide for installing an app version from a GitHub release feed or a Gitea / Forgejo release feed (including Codeberg) instead of — or alongside — the Nextcloud App Store. Two equivalent paths: click through the App Versions UI, or POST to the OCS API for scripted / CI use.
Goal
By the end of this tutorial you will have:
- Understood the difference between the three source kinds App Versions supports (
appstore,github-release,gitea-release). - Reviewed and, if needed, extended the
trusted_sourcesallowlist so App Versions is permitted to fetch from your chosen source. - Bound one installed app to a
github-releaseORgitea-releasesource — either via the App Versions UI's Version source card or via the OCSPOST /api/source/{appId}/bindendpoint. - Verified in the App Versions UI that the versions dropdown now shows releases from the new source.
- Installed a specific version from the new source.
The canonical worked example throughout is: bind opencatalogi to codeberg.org/Conduction/opencatalogi and install the latest dev-release.
Which path — UI or API?
App Versions ships a first-class UI for source binding as of 1.2.0. Both paths hit the same endpoint (POST /api/source/{appId}/bind) with the same payload shape — pick whichever fits the situation:
- UI — recommended for one-off admin actions. Open
/apps/app_versions/, pick an installed app, use the quick-switch buttons in the Version source card (App Store·Codeberg·GitHub), or click Advanced… to override host / owner / repo / asset pattern. The Codeberg and GitHub quick-switch buttons pre-fill Conduction's defaults (codeberg.org/Conduction/{appId}andConductionNL/{appId}respectively) so a single click is enough for Conduction apps. - API — recommended for automation, CI, config-management, or any environment where clicking a button doesn't fit. The step-by-step below walks the API flow.
Steps 1 (trust list) and 2 (find the identifier) apply to both paths. Step 3 shows both the UI and the API side by side.
When to use this
Use an alternate source when the version you need is not available on the Nextcloud App Store. Common cases:
- Per-push dev builds — a Conduction app publishes signed
.tar.gzreleases to Codeberg for every merge intodevelopmentwithout pushing them to the App Store. Staging/canary environments consume those to test unreleased code. - Private repositories — apps developed inside a company can publish releases to a private GitHub / Gitea repo. App Versions supports PATs (Personal Access Tokens) for those; see the PAT reference.
- Forks or beta channels — you maintain a fork with a hotfix your organisation needs before it lands upstream.
The App Store binding (the default) is always available in parallel — App Versions merges versions from every configured source into the same dropdown.
Prerequisites
- App Versions ≥ 1.1.0 installed and enabled on your Nextcloud (
gitea-releasesupport was added in 1.1.0). Check with:sudo -u www-data php occ app:list | grep app_versions - Admin rights on the Nextcloud. All bind endpoints require admin +
#[PasswordConfirmationRequired]. - Network reachability — the Nextcloud host must be able to reach
api.github.com(for GitHub) or the Gitea/Forgejo host you're binding to. - The target app must already be installed — App Versions binds sources per installed app. Install the app once from any channel first, then rebind.
The three source kinds
| Kind | Source-ID shape | Example | Backing driver |
|---|---|---|---|
appstore | appstore | (implicit) | AppStoreSource — reads the Nextcloud App Store info JSON |
gitea-release (recommended) | gitea:<host>/<owner>/<repo> | gitea:codeberg.org/Conduction/opencatalogi | GiteaReleaseSource — reads https://<host>/api/v1/repos/<owner>/<repo>/releases |
github-release | github:<owner>/<repo> | github:ConductionNL/openregister | GithubReleaseSource — reads https://api.github.com/repos/<owner>/<repo>/releases |
For Conduction apps, gitea-release pointed at codeberg.org/Conduction/<app> is the recommended default — that's the source of truth after the ConductionNL GitHub → Codeberg migration. github-release is retained for apps that still publish releases to GitHub.
Every source binding stores an optional assetPattern (default *.tar.gz) that decides which release asset to download.
Steps
1. Verify or extend the trust allowlist
Before it fetches anything, App Versions runs the source identifier against the trusted_sources allowlist. If the owner/repo (or host/owner/repo, for Gitea) does not match any pattern, the bind is rejected with HTTP 403 — no network call is made.
Inspect the current allowlist:
sudo -u www-data php occ config:app:get app_versions trusted_sources
Empty response = the built-in defaults apply, which are:
codeberg.org/Conduction/*
ConductionNL/*
If your target does not match, extend the list. Setting the value replaces the entire list — always include the built-ins you want to keep:
sudo -u www-data php occ config:app:set app_versions trusted_sources \
'["codeberg.org/Conduction/*","ConductionNL/*","myorg/*","gitea.example.com/team/*"]'
Glob wildcards are shell-style (* matches any characters except /). One pattern per line — no whitespace in the JSON.
2. Find the exact source identifier
For GitHub: browse to the repo on github.com and read the owner/repo from the URL — e.g. https://github.com/ConductionNL/openregister → github:ConductionNL/openregister.
For Gitea / Forgejo / Codeberg: same principle, but include the host — e.g. https://codeberg.org/Conduction/opencatalogi → gitea:codeberg.org/Conduction/opencatalogi.
Optional: verify the API responds with releases before you bind. For Codeberg:
curl -s "https://codeberg.org/api/v1/repos/Conduction/opencatalogi/releases?limit=5" | jq '.[].tag_name'
You should see a list of tag names (v1.0.4, v1.0.5-dev.20260708205504, …). If the response is [] or an error, fix that before binding — App Versions will show an empty dropdown otherwise.
3. Bind the app to the source
The bind endpoint is POST /ocs/v2.php/apps/app_versions/api/source/{appId}/bind. Payload varies by kind — full field list in openapi.json, summary below:
| Kind | Required fields | Optional |
|---|---|---|
appstore | kind=appstore | — |
gitea-release | kind=gitea-release, host, owner, repo | assetPattern |
github-release | kind=github-release, owner, repo | assetPattern |
Path A — via the UI (recommended for one-offs)
- Open
https://YOUR-NEXTCLOUD/apps/app_versions/in the browser. - Pick the target app from the "Pick an installed App" list (e.g.
opencatalogi). - In the info panel on the right you'll see a Version source card with the current binding label (e.g. Nextcloud App Store) and four buttons:
App Store·Codeberg·GitHub·Advanced…. - Click Codeberg for the recommended path — this posts
{kind:"gitea-release", host:"codeberg.org", owner:"Conduction", repo:"{appId}", assetPattern:"*.tar.gz"}in the background and re-fetches the versions list from the new source. The active binding gets a blue highlight. - If your target repo is not
codeberg.org/Conduction/{appId}(self-hosted Gitea, different owner, private repo) click Advanced… instead — the dialog gives you full override ofkind,host,owner,repo, andassetPattern, pre-populated from the current binding. - On failure (403 from an untrusted source, 400 from a bad payload, network error) the response message surfaces inline in the dialog / card — the OCS meta status is not swallowed.
Skip to step 4 to verify.
Path B — via the OCS API (recommended for automation / CI)
Worked example — Codeberg via bash:
curl -u 'ADMIN_USER:ADMIN_PASSWORD' \
-H "OCS-APIRequest: true" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-X POST \
"https://YOUR-NEXTCLOUD/ocs/v2.php/apps/app_versions/api/source/opencatalogi/bind?format=json" \
-d '{
"kind": "gitea-release",
"host": "codeberg.org",
"owner": "Conduction",
"repo": "opencatalogi"
}'
Same example — Windows PowerShell:
curl.exe -u "ADMIN_USER:ADMIN_PASSWORD" -H "OCS-APIRequest: true" -H "Content-Type: application/json" -H "Accept: application/json" -X POST "https://YOUR-NEXTCLOUD/ocs/v2.php/apps/app_versions/api/source/opencatalogi/bind?format=json" -d '{\"kind\":\"gitea-release\",\"host\":\"codeberg.org\",\"owner\":\"Conduction\",\"repo\":\"opencatalogi\"}'
Two notes:
curl.exeon Windows. PowerShell'scurlis an alias forInvoke-WebRequestwith different flags — always callcurl.exeexplicitly, or useInvoke-RestMethodinstead.?format=json— OCS defaults to XML. Add this to get a JSON body back.
Expected 200 response (JSON):
{
"ocs": {
"meta": {"status":"ok","statuscode":200,"message":"OK"},
"data": {
"appId": "opencatalogi",
"sourceId": "gitea:codeberg.org/Conduction/opencatalogi",
"binding": {
"kind": "gitea-release",
"host": "codeberg.org",
"owner": "Conduction",
"repo": "opencatalogi",
"assetPattern": "*.tar.gz",
"boundAt": "2026-07-10T08:25:50+00:00"
}
}
}
}
4. Verify in the UI
If you took Path A (UI): the picker already updated in place — the Codeberg button carries the blue "active" highlight and the version list re-fetched immediately. Nothing more to do.
If you took Path B (API): open https://YOUR-NEXTCLOUD/apps/app_versions/ in the browser (hard refresh — Ctrl+Shift+R — to bypass caching). Pick OpenCatalogi from the "Pick an installed App" list. Two things should now be different:
- The Version source card reads
Codeberg / Gitea (codeberg.org/Conduction/opencatalogi)(previously Nextcloud App Store), with theCodebergquick-switch button highlighted. - The versions dropdown shows tags from Codeberg — including any
-beta.*and-dev.*releases that never made it to the App Store.
5. Install the version you want
Select a version from the dropdown and click Install. App Versions:
- Fetches the matching asset from the release (matching
assetPattern, default*.tar.gz). - Verifies the tarball's app-id and version match
appinfo/info.xml. - Optionally verifies a sibling
.sha256if one is published in the same release. - Extracts into the Nextcloud apps directory and runs the standard NC upgrade + repair steps.
- Records the new installation with the bound source-id so subsequent version checks use the same driver.
Verification
Confirm the install:
sudo -u www-data php occ app:list | grep opencatalogi
Expected: the version you selected. Reload the app in the browser and confirm the new build renders.
Common issues
| Symptom | Fix |
|---|---|
| HTTP 403 on bind with body mentioning "allowlist patterns" | The identifier is not in trusted_sources. Extend the allowlist (Step 1) and retry. |
| HTTP 404 on bind (Nextcloud login page returned) | URL is wrong. OCS routes live under /ocs/v2.php/apps/app_versions/api/..., not /apps/app_versions/api/.... |
| HTTP 401 or CSRF error | Password confirmation isn't fresh. Use basic auth (curl -u user:pw) or reauthenticate the session. |
| Dropdown empty after bind | (a) The upstream API returned zero releases — verify with the curl call in Step 2. (b) All releases are drafts — App Versions filters drafts. |
| "Multiple matching assets" error on install | The release publishes several *.tar.gz — set a more specific assetPattern in the binding (e.g. opencatalogi-*.tar.gz) so exactly one matches. |
| Install fails with "class not found" style errors after upgrade | Cross-app dependency — the source you installed depends on newer classes in another app. Upgrade that dependency (via App Versions itself) to a matching version. |
| GitHub rate-limit (HTTP 403 during listVersions) | Unauthenticated GitHub API is limited to 60/hr. Configure a PAT via POST /api/pats and it will be resolved automatically for matching owner/repo. |
Reference
- Full endpoint reference:
openapi.json - Source drivers:
lib/Service/Source/ - Trust list configuration:
lib/Service/Source/TrustedSourceList.php - Related tutorial: Manage settings
- Introduction of gitea-release: PR #21 — original motivation was staging OpenCatalogi dev builds onto a canary environment without shipping them to the public App Store.