Skip to content

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:

  1. A working IntroVox build (npm run build succeeds without errors)
  2. A GitHub repository with the code published
  3. A valid appinfo/info.xml with the correct version
  4. An up-to-date CHANGELOG.md
  5. Screenshots in docs/screenshots/
  6. The App Store certificate (one-time setup, below)
  7. 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.key securely. 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 .key files in App Store tarballs or source distributions.

Submit CSR for Approval

  1. Go to github.com/nextcloud/app-certificate-requests
  2. Open a new issue
  3. Paste the contents of introvox.csr (omit the BEGIN/END lines)
  4. Wait for approval (typically 1–2 days)
  5. The Nextcloud team commits a signed certificate (introvox.crt)

Register the App

After receiving the certificate:

  1. Go to apps.nextcloud.com/developer/register
  2. Log in with your GitHub account
  3. Upload introvox.crt
  4. 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.json now returns HTTP 302 to garm2.nextcloud.com. Without -L, the comparison silently fails on empty input and gives d41d8cd98f00b204e9800998ecf8427e (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)

python3 regenerate_js_translations.py

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 compiled js/ ships)
  • node_modules/ — dependencies
  • .git/ — git history
  • *.key, *.crt, *.pem — certificates and keys
  • deploy.sh and 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

  1. Go to github.com/nextcloud/IntroVox/releases
  2. Click Draft a new release
  3. Tag: vX.Y.Z
  4. Title: vX.Y.Z - Brief description
  5. Notes: copy the relevant section from CHANGELOG.md
  6. Upload introvox-X.Y.Z.tar.gz as a release asset
  7. 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:

https://github.com/nextcloud/IntroVox/releases/download/vX.Y.Z/introvox-X.Y.Z.tar.gz

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)

  1. Log in at apps.nextcloud.com
  2. Go to your developer dashboard → IntroVox → New Release
  3. URL pattern: https://apps.nextcloud.com/developer/apps/introvox/releases/new (only reachable when logged in as the app owner)
  4. Paste:
  5. Download URL — the GitHub release URL
  6. Signature — contents of introvox-X.Y.Z.sig
  7. 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:

  1. Log in at apps.nextcloud.com
  2. Click your username top-right → look for "API Token" / "Account" / "Profile"
  3. Generate a new token, copy it, overwrite the local token file
  4. 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, not npm install for reproducibility)
  • Check that the webpack build completes without errors or warnings
  • Verify js/main.js is at least ~100 KB (smaller usually means a build failure)

Signature Issues

  • Must be base64-encoded
  • Use the exact same .tar.gz file 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