Publishing apps
Publish any Android app with zsp and your Zapstore pubkey. A release goes live in minutes, once the catalog relay has verified it.
zsp is Zapstore’s command-line publishing tool. It fetches your APK, fills in the listing from your existing metadata, signs it with your pubkey, and publishes it to the catalog.
Any Android app can be published. It doesn’t need to use Nostr, and it can stay on GitHub, Google Play or F-Droid as well.
Install
go install github.com/zapstore/zsp/v2/cmd/zsp@latestOr download a binary. zsp -v shows the version you have.
Publish with the wizard
In your project folder, run zsp with no arguments. The wizard has three steps:
- Add your app. It asks where your APK is: a releases page, F-Droid, a URL or a local file.
- Prove it’s yours. It asks for your signing keystore once and signs a claim that links the app’s signing certificate to your pubkey.
- Publish. It writes a
zapstore.yamlfile to your folder. The catalog reads it to write the correct metadata for your app, so commit that file.
The last question is how new releases reach the catalog. You can let the catalog publish them for you from your public release source (recommended), or publish them yourself from CI/CD. You can always publish your own Nostr events instead.
1Your APK
Repository, F-Droid, a URL or a local file
2zsp
Reads it, fills in metadata, signs the listing with your pubkey
3Catalog relay
Checks your pubkey and the file
4The catalog
In the Android app and on the website
The other commands
zsp checkreads the catalog relay and nothing else. It finds your APK, says whether the app is listed, which claim is active for its certificate, and what to do next. Run it first when something goes wrong.zsp publishpublishes the release described inzapstore.yaml. It never prompts, so it works in CI. It needs an active claim that names your pubkey.zsp skillprints instructions that teach a coding agent how to use zsp.
check and publish print JSON when they are not run in a terminal. Flags and advanced use are in the zsp README.
Where the APK comes from
zsp detects the source from the URL.
GitHub, GitLab, Codeberg, Gitea, or Forgejo release assets:
repository: https://github.com/your-org/your-appF-Droid or IzzyOnDroid, with richer metadata from GitHub or the Play Store:
repository: https://github.com/AntennaPod/AntennaPod
release_source: https://f-droid.org/packages/de.danoeh.antennapodA local file:
release_source: ./build/outputs/apk/release/app-release.apk
repository: https://github.com/your-org/your-appAny URL that points to an APK:
release_source: https://example.com/downloads/app.apkFor a private repository, set a token in the environment: GITHUB_TOKEN, GITLAB_TOKEN or GITEA_TOKEN.
Metadata
Fields you set in zapstore.yaml always win. For the rest, the first source in metadata_sources that has a value wins. The name and icon come from the APK unless you set them. Pass --skip-metadata to skip remote metadata.
| Source | What it reads |
|---|---|
fastlane |
Your fastlane/metadata/android folder |
github |
Name, description, topics, license, website |
gitlab |
Name, description, topics, license |
gitea |
Fastlane files from Gitea, Forgejo and Codeberg |
fdroid |
Name, description, categories, icon, screenshots |
playstore |
Name, description, icon, screenshots |
Without metadata_sources, GitHub and GitLab try Fastlane first and then their own data. F-Droid and the Play Store are used only when you list them.
metadata_sources:
- playstore
- githubWhat the catalog relay requires
- One complete
arm64-v8aAPK per release. Support for 32-bit ARM (armeabi-v7a) is coming soon. - A
versionCodehigher than the last one you published for that app. zsp stops an equal or lower one. - An active claim that names your pubkey, or a delegate of it.
- The release channel is
mainunlesschannelsays otherwise.
A release waits on the catalog relay until these checks pass, which usually takes a few minutes. The relay reads the APK itself to check the certificate, the architecture and the version code. Nobody reviews it by hand. The phone checks the file again before it installs.
Claim your app
Zapstore’s indexer may already list your app from its public releases, under the catalog’s pubkey. A claim is what lets you take it over. It is a proof signed with your APK signing key (NIP-C1) that names your Zapstore pubkey, and the catalog relay checks it against the APK. The relay accepts your own listings for the app only with a current claim, so nobody can publish in your name without your signing key.
Once claimed, your listing takes priority over the indexer’s and the app page names you as the developer. Your name, followers and zaps now attach to the certificate, and to any certificate you link to it with a signing lineage. If you skip publishing a release, the indexer can still fill it in from your public source, under your listing.
A claim generally lasts two years. The wizard renews it when you run it within 30 days of the end. You need the keystore (.jks, .p12, or .pem) only for the wizard. zsp publish never reads it.
Your pubkey signs the listing and the claim. It does not replace the APK signing certificate: the pubkey says who published the listing, and the certificate decides what Android accepts as an update.
Claim from the app page
You can also claim an app from its page on this website. Sign in with your pubkey and open the app: an unclaimed app shows Claim this app at the top right. The dialog offers zsp, or two keytool and openssl commands that sign a message with your certificate on your own machine. You paste back two public lines, signature: and cert:, and the page checks them before your pubkey signs the claim. Your keystore, its password and your secret key never reach the website.
Once you hold the claim, the same page shows Renew proof and Edit listing.
Claim with your best pubkey
Claim with the pubkey people already know you by: the one with your follows, your reputation and your Lightning address. Don’t create a new one just for publishing.
- A pubkey with a history usually passes the catalog’s web of trust check. A new one usually doesn’t.
- Zaps for your app go to the pubkey on the claim, at the Lightning address (
lud16) on its profile. A listing has no zap field, and a delegate receives nothing. - People who follow you see your app under the name they recognise.
For CI or a build machine, keep that pubkey’s secret key off the machine and use a delegate instead (see CI/CD).
How you sign
Set SIGN_WITH before zsp publish, or put it in a .env file in the folder you run zsp from.
| Method | Value |
|---|---|
| Remote signer (recommended) | SIGN_WITH=bunker://pubkey?relay=...&secret=... |
| Secret key | SIGN_WITH=nsec1... |
zsp refuses a hex key and has no browser signing. It also refuses a .env containing SIGN_WITH that other users on the machine can read, so keep it owner-only and out of Git.
CI/CD
If your release source is public, you may not need CI. Zapstore’s indexer follows it and publishes each new release, shown as developed by you.
Otherwise, the wizard can add a second pubkey to your claim as a delegate. A delegate can publish releases but cannot change who owns the app. Give CI the delegate’s nsec, or a bunker:// URL for it, as SIGN_WITH, stored as a secret. Trust and zaps still go to your own pubkey. To rotate the delegate, run zsp again with a new one.
If your signing key changes
Android accepts an update signed with a new certificate only when the new certificate carries a signing lineage (APK Signature Scheme v3) back to the old one. Rotate that way, and updates keep working. The catalog relay rejects a release whose certificate is neither the app’s current one nor linked to it by lineage.
After a rotation, run zsp again to claim the app with the new certificate. Without a lineage, people who have the app see a certificate mismatch and cannot update without reinstalling.
After you publish
A new listing appears once the catalog relay has read the APK and checked your claim, which takes a few minutes. Link to it at /apps/ followed by your package name, from your website or release notes.
Your app’s page shows its zaps and comments, and your profile’s Activity lists what people sent you. Download counts are not available to developers yet.
Troubleshooting
Run zsp check first. It says why the app is not accepted and what to do next. The error zsp publish prints also names the check that failed.
- The catalog relay refused your first publish. Usually no claim for the app’s signing certificate names your pubkey. Run zsp to create one. A new app may also need to pass the catalog’s admission check.
- The catalog relay rejected a release. Check the list in What the catalog relay requires.
rate-limited: retry in …. A pubkey can add at most 3 new apps a day, and one IP address can send at most 10 releases an hour. Wait and try again.- The APK was not found. The release needs an
.apkasset. If several APKs match, narrow them withmatchinzapstore.yaml. Check the repository URL, and the token for a private repository. - The app is not in search. Confirm
zsp publishsucceeded, then wait a few minutes.zsp checksays whether the app is listed. - The metadata is wrong. Fix
zapstore.yamland publish again. Your file overrides metadata from GitHub, F-Droid or the Play Store. - You published from the wrong pubkey. Delete the event from that pubkey, then publish from the right one.