Nextcloud App Store Submission¶
This guide walks through submitting an IntroVox release to the Nextcloud App Store. For the full per-release checklist see RELEASE_CHECKLIST.md in the repository root.
Prerequisites¶
Before submitting a release you need:
- A working IntroVox build (
npm run buildsucceeds without errors) - A GitHub repository with the code published
- A valid
appinfo/info.xmlwith the correct version - An up-to-date
CHANGELOG.md - Screenshots in
docs/screenshots/ - The App Store certificate (one-time setup, below)
- The signing key (paired with the certificate above)
One-Time Setup: App Store Certificate¶
The Nextcloud App Store requires a certificate to verify your identity and sign each release.
Generate Private Key and CSR¶
# Generate the private key (KEEP SECRET — never commit!)
openssl genrsa -out introvox.key 4096
# Generate a Certificate Signing Request
openssl req -new -key introvox.key -out introvox.csr \
-subj "/CN=introvox"
⚠️ Store
introvox.keysecurely. Without this key, you cannot upload new releases. Recommended storage: encrypted USB backup plus a local working copy in a directory excluded from git. Never include.keyfiles in App Store tarballs or source distributions.
Submit CSR for Approval¶
- Go to github.com/nextcloud/app-certificate-requests
- Open a new issue
- Paste the contents of
introvox.csr(omit the BEGIN/END lines) - Wait for approval (typically 1–2 days)
- The Nextcloud team commits a signed certificate (
introvox.crt)
Register the App¶
After receiving the certificate:
- Go to apps.nextcloud.com/developer/register
- Log in with your GitHub account
- Upload
introvox.crt - Sign a challenge to prove ownership of the private key
Per-Release: Verify the Certificate Pair¶
Before every release, verify the signing key still matches the App Store certificate:
# MD5 of local signing key's public component
openssl rsa -in introvox.key -pubout 2>/dev/null | openssl md5
# MD5 of the App Store certificate's public key (must follow redirects!)
curl -sL "https://apps.nextcloud.com/api/v1/apps.json" | \
python3 -c "import json,sys; [print(a['certificate']) for a in json.load(sys.stdin) if a['id']=='introvox']" | \
openssl x509 -pubkey -noout 2>/dev/null | openssl md5
The two MD5 hashes must be identical. If they differ, the certificate has been replaced (e.g., revoked and reissued) and your local key is no longer valid.
Don't request a new certificate unnecessarily — issuing a new one automatically revokes the old one and breaks all existing tooling.
Always use
curl -sL(follow redirects) —apps.nextcloud.com/api/v1/apps.jsonnow returns HTTP 302 togarm2.nextcloud.com. Without-L, the comparison silently fails on empty input and givesd41d8cd98f00b204e9800998ecf8427e(the MD5 of an empty string).
Per-Release: Build the Release Package¶
Build the App¶
# Clean previous builds
rm -rf js/
rm -f introvox-*.tar.gz
# Install dependencies
npm ci
# Production build
npm run build
# Verify build output
ls -lh js/
Regenerate Translations (if needed)¶
Create the Tarball¶
Important: the tarball's root folder must be introvox (lowercase, no version suffix).
TEMP_DIR=$(mktemp -d) && \
mkdir -p "$TEMP_DIR/introvox" && \
cp -r appinfo lib l10n templates css img js "$TEMP_DIR/introvox/" && \
cp CHANGELOG.md LICENSE README.md "$TEMP_DIR/introvox/" && \
cd "$TEMP_DIR" && \
tar -czf introvox-X.Y.Z.tar.gz introvox && \
mv introvox-X.Y.Z.tar.gz /path/to/IntroVox/ && \
rm -rf "$TEMP_DIR"
Exclude These From the Tarball¶
src/— source code (only compiledjs/ships)node_modules/— dependencies.git/— git history*.key,*.crt,*.pem— certificates and keysdeploy.shand other deployment scripts with server details- Any test/sample data directories
Tarball Security Check¶
Verify no sensitive content slipped in:
# List all files
tar -tzf introvox-X.Y.Z.tar.gz | grep -iE '(internal|credential|\.key|\.env|deploy)'
For content scanning, extract the tarball and grep -r per file extension. Don't pipe tar -xzf -O into one big blob — webpack-minified bundle bytes can coincidentally match patterns like Math.pow(2,...) and trigger false positives on substrings like password=.
Sign the Tarball¶
openssl dgst -sha512 -sign introvox.key introvox-X.Y.Z.tar.gz | openssl base64 -A > introvox-X.Y.Z.sig
The signature must be base64 encoded with no newlines. Verify with wc -c introvox-X.Y.Z.sig — it should be a single long line.
Per-Release: Publish on GitHub¶
- Go to github.com/nextcloud/IntroVox/releases
- Click Draft a new release
- Tag:
vX.Y.Z - Title:
vX.Y.Z - Brief description - Notes: copy the relevant section from
CHANGELOG.md - Upload
introvox-X.Y.Z.tar.gzas a release asset - Publish
Or use the CLI:
gh release create vX.Y.Z introvox-X.Y.Z.tar.gz \
--repo nextcloud/IntroVox \
--title "vX.Y.Z - Description" \
--notes-file <(sed -n '/^## \['"X.Y.Z"'/,/^## /p' CHANGELOG.md | head -n -1)
The resulting download URL is:
Per-Release: Submit to the App Store¶
There are two routes — try the API first, fall back to the web UI if the token is rejected.
Route A — API Upload (Preferred)¶
TOKEN=$(tr -d '[:space:]' < /path/to/appstore-api-token.txt)
SIG=$(cat introvox-X.Y.Z.sig)
DOWNLOAD_URL="https://github.com/nextcloud/IntroVox/releases/download/vX.Y.Z/introvox-X.Y.Z.tar.gz"
curl -s -w "\nHTTP %{http_code}\n" -X POST \
-H "Authorization: Token $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"download\":\"$DOWNLOAD_URL\",\"signature\":\"$SIG\",\"nightly\":false}" \
https://apps.nextcloud.com/api/v1/apps/releases
- HTTP 200 — success
- HTTP 403 "You do not have permission" — the token is expired or revoked. Go to Route B and refresh the token afterwards.
Route B — Web UI Upload (Fallback)¶
- Log in at apps.nextcloud.com
- Go to your developer dashboard → IntroVox → New Release
- URL pattern:
https://apps.nextcloud.com/developer/apps/introvox/releases/new(only reachable when logged in as the app owner) - Paste:
- Download URL — the GitHub release URL
- Signature — contents of
introvox-X.Y.Z.sig - Release notes — copy the relevant CHANGELOG section
Refreshing the API Token¶
The API-token page used to be at apps.nextcloud.com/account/api-token but that URL has 404'd at least once (May 2026). To find a fresh token:
- Log in at apps.nextcloud.com
- Click your username top-right → look for "API Token" / "Account" / "Profile"
- Generate a new token, copy it, overwrite the local token file
- Retry Route A — should return HTTP 200
Wait for Approval¶
The Nextcloud team reviews submissions. This can take days to weeks. They check:
- Code quality
- Security
- Compliance with App Store guidelines
- Proper use of Nextcloud APIs
Common Issues¶
Certificate Mismatch¶
If the §"Verify the Certificate Pair" MD5 comparison shows different hashes, your local key no longer matches the App Store certificate. Do not generate a new certificate unless absolutely necessary — investigate first (was the key replaced? is there a USB backup of the right key?).
Build Issues¶
- Ensure all dependencies are installed (
npm ci, notnpm installfor reproducibility) - Check that the webpack build completes without errors or warnings
- Verify
js/main.jsis at least ~100 KB (smaller usually means a build failure)
Signature Issues¶
- Must be base64-encoded
- Use the exact same
.tar.gzfile that was uploaded to GitHub — re-signing a regenerated tarball produces a different signature - No newlines in the signature (one long line)
API Token Expired¶
The IntroVox API token at appstore-api-token.txt has expired silently in the past (no notification — first noticed when v1.4.3 release returned HTTP 403). Always have Route B (web UI) ready as a fallback.
See Also¶
- Release Process — full per-release flow
- RELEASE_CHECKLIST.md — authoritative per-release checklist
- Installation — installing IntroVox on a Nextcloud instance