zsp
A fast CLI tool for publishing Android apps to Nostr relays. Used by Zapstore.
Features
- APK acquisition from GitHub, GitLab, Codeberg, F-Droid, web pages, or local files
- APK parsing to extract package info, version, certificate fingerprint, icon, and permissions
- Metadata enrichment from GitHub, GitLab, F-Droid, or Google Play Store
- Blossom uploads for icons, screenshots, APKs
- Nostr event signing via private key, NIP-46 bunker, or browser extension (NIP-07)
- Relay publishing of compliant software events
Installation
From Source
go install github.com/zapstore/zsp@latest
Pre-built Binaries
Download from releases.
Quick Start
zsp publish --wizard
The interactive wizard guides you through the setup process and helps determine the best options for your app.
APK Sources
zsp supports multiple sources for fetching APKs. The source type is auto-detected from URLs.
GitHub Releases
Fetches APKs from GitHub release assets. Automatically selects the best arm64-v8a APK.
repository: https://github.com/AeonBTC/mempal
zsp publish -r github.com/AeonBTC/mempal
GitLab Releases
Fetches APKs from GitLab release links.
repository: https://gitlab.com/AuroraOSS/AuroraStore
For self-hosted GitLab without "gitlab" in the domain:
release_source:
url: https://git.mycompany.com/team/app
type: gitlab
Codeberg / Gitea / Forgejo
Fetches APKs from Gitea-compatible forges.
repository: https://codeberg.org/Freeyourgadget/Gadgetbridge
F-Droid
Fetches APKs from F-Droid or IzzyOnDroid repositories.
# APKs from F-Droid, source code from GitHub
repository: https://github.com/AntennaPod/AntennaPod
release_source: https://f-droid.org/packages/de.danoeh.antennapod
Web Sources
Fetch APKs from any URL with version extraction via CSS selectors, JSON APIs, or HTTP headers.
# Extract version from HTML using CSS selector
repository: https://github.com/AntennaPod/AntennaPod
release_source:
version:
url: https://f-droid.org/packages/de.danoeh.antennapod/
selector: ".package-version-header"
match: "([0-9.]+)"
asset_url: https://f-droid.org/repo/de.danoeh.antennapod_{version}.apk
Direct APK URL (no scraping):
release_source: https://example.com/downloads/app.apk
Local Files
Publish a local APK file.
release_source: ./build/outputs/apk/release/app-release.apk
repository: https://github.com/user/app
zsp publish app.apk -r github.com/user/app
zsp can fetch app metadata from external sources to enrich your publication.
Sources
| Source |
Data Retrieved |
github |
Name, description, topics, license, website, README |
gitlab |
Name, description, topics, license |
fdroid |
Name, description, summary, categories, icon, screenshots |
playstore |
Name, description, icon, screenshots |
Priority
When multiple sources are used, metadata is merged with this priority:
- YAML config (always wins)
- APK metadata (app label)
- Play Store
- Others
Usage
# CLI flags (can be repeated)
zsp publish -m github -m playstore
# Or in YAML
metadata_sources:
- playstore
- github
Configuration Reference
Minimal Config
repository: https://github.com/user/app
Full Config
# ═══════════════════════════════════════════════════════════════════
# SOURCE CONFIGURATION
# ═══════════════════════════════════════════════════════════════════
# Source code repository URL or NIP-34 naddr (for display in app store)
repository: https://github.com/user/app
# Where to fetch APKs (if different from repository)
# Can be URL string, local path, or object with version extractor
# Local paths: ./build/app-release.apk, ../builds/*.apk
release_source: https://f-droid.org/packages/com.example.app
# Regex pattern to filter APK assets from releases
# (rarely needed - system auto-selects best arm64-v8a APK)
match: ".*arm64.*\\.apk$"
# ═══════════════════════════════════════════════════════════════════
# APP METADATA
# ═══════════════════════════════════════════════════════════════════
# App name (overrides APK label)
name: My App
# Short one-line description
summary: A wonderful app for doing things
# Full description (supports markdown)
description: |
My App is a powerful tool that helps you accomplish your goals.
Features:
- Feature one
- Feature two
# Category tags
tags:
- productivity
- tools
- nostr
# SPDX license identifier
license: MIT
# App homepage
website: https://myapp.example.com
# ═══════════════════════════════════════════════════════════════════
# MEDIA
# ═══════════════════════════════════════════════════════════════════
# App icon (local path or URL, otherwise extracted from APK)
icon: ./assets/icon.png
# Screenshots (local paths or URLs)
images:
- ./screenshots/screen1.png
- https://example.com/screenshot2.png
# ═══════════════════════════════════════════════════════════════════
# RELEASE CONFIGURATION
# ═══════════════════════════════════════════════════════════════════
# Release notes file or URL (extracts section matching version if Keep a Changelog format)
release_notes: ./CHANGELOG.md
# ═══════════════════════════════════════════════════════════════════
# NOSTR-SPECIFIC
# ═══════════════════════════════════════════════════════════════════
# Nostr NIPs supported by this app (for Nostr clients)
supported_nips:
- "01"
- "07"
- "46"
# Minimum version code users should update to
min_allowed_version_code: 100
# ═══════════════════════════════════════════════════════════════════
# VARIANTS
# ═══════════════════════════════════════════════════════════════════
# APK variant patterns (for apps with multiple builds)
variants:
fdroid: ".*-fdroid-.*\\.apk$"
google: ".*-google-.*\\.apk$"
# ═══════════════════════════════════════════════════════════════════
# METADATA SOURCES
# ═══════════════════════════════════════════════════════════════════
# External sources for metadata enrichment
# Note: metadata is fetched automatically for new releases (use --skip-metadata to disable)
metadata_sources:
- playstore
- fdroid
CLI Reference
Usage Patterns
zsp publish [config.yaml] # Config file (default: ./zapstore.yaml)
zsp publish <app.apk> [-r <repo>] # Local APK with optional source repo
zsp publish -r <repo> # Fetch latest release from repo
zsp publish --wizard # Interactive wizard
zsp apk --extract <app.apk> # Extract APK metadata as JSON
zsp identity --link-key <cert> # Link signing key to Nostr identity
Flags
| Flag |
Description |
-r <url> |
Source code repository URL (GitHub/GitLab/Codeberg). Also fetches releases from here unless -s is specified. |
-s <url> |
Release/download source URL (F-Droid, web page, etc). Use alone for closed-source apps. |
-m <source> |
Fetch metadata from source (repeatable). Fetched automatically for new releases. |
-y |
Auto-confirm all prompts |
--offline |
Sign events without uploading/publishing (outputs JSON to stdout) |
-h, --help |
Show help |
-v, --version |
Print version |
| Flag |
Description |
--wizard |
Run interactive wizard (recommended for first-time setup) |
--match <pattern> |
Regex pattern to filter APK assets (rarely needed - system auto-selects best APK) |
--commit <hash> |
Git commit hash for reproducible builds |
--channel <name> |
Release channel: main (default), beta, nightly, dev |
--check |
Verify config fetches arm64-v8a APK (exit 0=success) |
--skip-preview |
Skip the browser preview prompt |
--port <port> |
Custom port for browser preview/signing |
--overwrite-release |
Bypass cache, re-publish unchanged release |
--skip-metadata |
Skip fetching metadata from external sources (useful for frequent releases) |
--app-created-at-release |
Set kind 32267 created_at to the release timestamp (indexer compatibility) |
--quiet |
Minimal output, no prompts (implies -y) |
--verbose |
Debug output |
--no-color |
Disable colored output |
Environment Variables
| Variable |
Required |
Description |
SIGN_WITH |
Yes |
Signing method (see below) |
GITHUB_TOKEN |
No |
GitHub API token (avoids rate limits) |
RELAY_URLS |
No |
Comma-separated relay URLs |
BLOSSOM_URL |
No |
Custom Blossom CDN server |
Defaults
- RELAY_URLS:
wss://relay.zapstore.dev
- BLOSSOM_URL:
https://cdn.zapstore.dev
Signing Methods
Private Key (nsec)
Direct signing with a Nostr private key.
SIGN_WITH=nsec1... zsp publish zapstore.yaml
⚠️ Security: Private keys in environment variables can be exposed via /proc/*/environ on Linux or shell history. For production, prefer bunker or browser signing.
Hex Private Key
64-character hex private key (converted to nsec internally).
SIGN_WITH=0123456789abcdef... zsp publish zapstore.yaml
Public Key (npub) - Unsigned Output
Output unsigned events for external signing workflows.
SIGN_WITH=npub1... zsp publish zapstore.yaml > unsigned-events.json
NIP-46 Bunker (Remote Signing)
Sign via a remote signer like nsecBunker.
SIGN_WITH="bunker://pubkey?relay=wss://relay.example.com&secret=..." zsp publish
Browser Extension (NIP-07)
Sign using your browser's Nostr extension (Alby, nos2x, Flamingo, etc.).
SIGN_WITH=browser zsp publish
This opens a browser window where you approve signing. Supports batch signing for efficiency.
Nostr Events
zsp publishes three NIP-82 compliant event types:
Kind 32267 - Software Application
App metadata (name, description, icon, screenshots, platforms).
{
"kind": 32267,
"tags": [
["d", "com.example.app"],
["name", "My App"],
["summary", "A wonderful app"],
["icon", "https://cdn.zapstore.dev/abc123..."],
["image", "https://cdn.zapstore.dev/def456..."],
["t", "productivity"],
["f", "android-arm64-v8a"],
["license", "MIT"],
["repository", "https://github.com/user/app"],
["h", "acfeaea6e51420e8068fac446ca9d17d7a9ef6a5d20d93894e50fee3d4902a84"]
],
"content": "Full app description..."
}
Kind 30063 - Software Release
Version information and references to assets.
{
"kind": 30063,
"tags": [
["d", "com.example.app@1.2.3"],
["i", "com.example.app"],
["version", "1.2.3"],
["c", "main"],
["e", "<asset-event-id>", "wss://relay.zapstore.dev"]
],
"content": "Release notes..."
}
Kind 3063 - Software Asset
Binary metadata (hash, size, certificate, URLs).
{
"kind": 3063,
"tags": [
["i", "com.example.app"],
["x", "sha256hash..."],
["version", "1.2.3"],
["version_code", "123"],
["url", "https://github.com/.../app.apk"],
["m", "application/vnd.android.package-archive"],
["size", "12345678"],
["f", "android-arm64-v8a"],
["apk_certificate_hash", "certsha256..."],
["min_platform_version", "21"],
["target_platform_version", "34"]
]
}
APK Selection
When a release contains multiple APKs, zsp uses smart ranking to select the best one:
- Architecture filtering: Removes x86, x86_64, armeabi-v7a (prefers arm64-v8a)
- Pattern matching: Applies
match regex if configured
- ML-based ranking: Scores APKs by filename patterns (universal, arm64, etc.)
- Interactive selection: In interactive mode, presents ranked options
Match Patterns
# Only mainnet builds
match: ".*-mainnet\\.apk$"
# Only arm64 builds
match: ".*arm64.*\\.apk$"
# Exclude debug builds
match: "^(?!.*debug).*\\.apk$"
CI/CD Integration
GitHub Actions
- name: Publish to Zapstore
env:
SIGN_WITH: ${{ secrets.BUNKER_URL_OR_NSEC }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
zsp publish -y zapstore.yaml
Check Mode
Verify your config fetches a valid APK without publishing:
zsp publish --check zapstore.yaml
# Exit 0 = success, prints package ID
# Exit 1 = failure
Offline Mode
--offline makes no network calls. It signs events locally and outputs them to stdout (pipeable to nak), with an upload manifest on stderr. A local APK path is required — remote sources (GitHub, F-Droid, etc.) are rejected.
A config file works in offline mode as long as release_source points to a local path. This is also the only way to supply local icons and screenshots.
# Save signed events for later
zsp publish -q --offline app.apk > events.json
# Pipe directly to nak for publishing (use -q for clean output)
zsp publish -q --offline app.apk | nak event wss://relay.zapstore.dev
# With npub (outputs unsigned events)
SIGN_WITH=npub1... zsp publish -q --offline app.apk > unsigned-events.json
# Config file with local APK, icon, and screenshots
zsp publish -q --offline zapstore.yaml > events.json
Example zapstore.yaml for offline use:
release_source: ./build/app-release.apk
repository: https://github.com/user/app
icon: ./assets/icon.png
images:
- ./screenshots/screen1.png
- ./screenshots/screen2.png
The manifest (on stderr) shows which files must be uploaded to Blossom before the events become valid:
Make sure to upload these files to https://cdn.zapstore.dev before publishing events:
APK:
Path: /path/to/app-release.apk
SHA256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
URL: https://cdn.zapstore.dev/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
Icon:
Path: /tmp/zsp_icon_a1b2c3d4e5f67890
SHA256: a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd
URL: https://cdn.zapstore.dev/a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd
Advanced Examples
repository: https://github.com/AntennaPod/AntennaPod
release_source: https://f-droid.org/packages/de.danoeh.antennapod
metadata_sources:
- playstore
Multi-variant App (e.g., F-Droid + Google Play builds)
repository: https://github.com/niccokunzmann/mundraub-android
match: ".*-fdroid-.*\\.apk$"
variants:
fdroid: ".*-fdroid-.*\\.apk$"
google: ".*-google-.*\\.apk$"
Self-hosted GitLab
release_source:
url: https://git.mycompany.com/mobile/app
type: gitlab
Web Source with JSON API
release_source:
version:
url: https://api.example.com/releases/latest
path: "$.tag_name"
match: "v([0-9.]+)"
asset_url: https://cdn.example.com/releases/app-v{version}.apk
Reproducible Build with Commit Hash
zsp publish --commit a1b2c3d4e5f6 zapstore.yaml
NIP-34 Repository Reference
repository: naddr1qqxnzd3exsmnjd3exqunjv...
zsp apk --extract app.apk
Outputs JSON with package ID, version, certificate hash, architectures, permissions, and extracts icon to disk.
License
MIT