A fast CLI tool for publishing Android apps to Nostr relays. Used by Zapstore.
- 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
go install github.com/zapstore/zsp@latestDownload from releases.
zsp publish --wizardThe interactive wizard guides you through the setup process and helps determine the best options for your app.
zsp supports multiple sources for fetching APKs. The source type is auto-detected from URLs.
Fetches APKs from GitHub release assets. Automatically selects the best arm64-v8a APK.
repository: https://github.com/AeonBTC/mempalzsp -r github.com/AeonBTC/mempalFetches APKs from GitLab release links.
repository: https://gitlab.com/AuroraOSS/AuroraStoreFor self-hosted GitLab without "gitlab" in the domain:
release_source:
url: https://git.mycompany.com/team/app
type: gitlabFetches APKs from Gitea-compatible forges.
repository: https://codeberg.org/Freeyourgadget/GadgetbridgeFetches 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.antennapodFetch APKs from any web page using regex patterns.
repository: https://github.com/AntennaPod/AntennaPod
release_source:
url: https://f-droid.org/packages/de.danoeh.antennapod/
asset_url: https://f-droid\.org/repo/de\.danoeh\.antennapod_[0-9]+\.apkDirect APK URL (no scraping):
release_source: https://example.com/downloads/app.apkPublish a local APK file.
release_source: ./build/outputs/apk/release/app-release.apk
repository: https://github.com/user/appzsp app.apk -r github.com/user/appzsp can fetch app metadata from external sources to enrich your publication.
| Source | Data Retrieved |
|---|---|
github |
Name, description, topics, license, website, README |
gitlab |
Name, description, topics, license |
fdroid |
Name, summary, description, categories, icon, screenshots |
playstore |
Name, description, icon, screenshots |
When multiple sources are used, metadata is merged with this priority:
- YAML config (always wins)
- APK metadata (app label)
- Play Store
- Others
# CLI flags (can be repeated)
zsp -m github -m playstore
# Or in YAML
metadata_sources:
- playstore
- githubrepository: https://github.com/user/app# ═══════════════════════════════════════════════════════════════════
# 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 type/asset_url for web scraping
# 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
# Release channel: main (default), beta, nightly, dev
release_channel: main
# Git commit hash for reproducible builds
commit: abc123def456
# ═══════════════════════════════════════════════════════════════════
# NOSTR-SPECIFIC
# ═══════════════════════════════════════════════════════════════════
# Nostr NIPs supported by this app (for Nostr clients)
supported_nips:
- "01"
- "07"
- "46"
# Minimum version users should update to
min_allowed_version: "1.0.0"
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
- fdroidzsp [config.yaml] # Config file (default: ./zapstore.yaml)
zsp <app.apk> [-r <repo>] # Local APK with optional source repo
zsp -r <repo> # Fetch latest release from repo
zsp apk --extract <app.apk> # Extract APK metadata as JSON
zsp # Interactive wizard
zsp --wizard # Wizard with existing config as defaults| 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 |
-n, --dry-run |
Parse & build events without publishing |
-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 |
--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) |
--quiet |
Minimal output, no prompts (implies -y) |
--verbose |
Debug output |
--no-color |
Disable colored output |
| 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 |
- RELAY_URLS:
wss://relay.zapstore.dev - BLOSSOM_URL:
https://cdn.zapstore.dev
Direct signing with a Nostr private key.
SIGN_WITH=nsec1... zsp zapstore.yaml
⚠️ Security: Private keys in environment variables can be exposed via/proc/*/environon Linux or shell history. For production, prefer bunker or browser signing.
64-character hex private key (converted to nsec internally).
SIGN_WITH=0123456789abcdef... zsp zapstore.yamlOutput unsigned events for external signing workflows.
SIGN_WITH=npub1... zsp zapstore.yaml > unsigned-events.jsonSign via a remote signer like nsecBunker.
SIGN_WITH="bunker://pubkey?relay=wss://relay.example.com&secret=..." zspSign using your browser's Nostr extension (Alby, nos2x, Flamingo, etc.).
SIGN_WITH=browser zspThis opens a browser window where you approve signing. Supports batch signing for efficiency.
zsp publishes three NIP-82 compliant event types:
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"]
],
"content": "Full app description..."
}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..."
}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"]
]
}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
matchregex if configured - ML-based ranking: Scores APKs by filename patterns (universal, arm64, etc.)
- Interactive selection: In interactive mode, presents ranked options
# Only mainnet builds
match: ".*-mainnet\\.apk$"
# Only arm64 builds
match: ".*arm64.*\\.apk$"
# Exclude debug builds
match: "^(?!.*debug).*\\.apk$"- name: Publish to Zapstore
env:
SIGN_WITH: ${{ secrets.NOSTR_NSEC }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
zsp -y zapstore.yamlVerify your config fetches a valid APK without publishing:
zsp publish --check zapstore.yaml
# Exit 0 = success, prints package ID
# Exit 1 = failureBuild events without publishing:
zsp --dry-run zapstore.yamlrepository: https://github.com/AntennaPod/AntennaPod
release_source: https://f-droid.org/packages/de.danoeh.antennapod
metadata_sources:
- playstorerepository: https://github.com/niccokunzmann/mundraub-android
match: ".*-fdroid-.*\\.apk$"
variants:
fdroid: ".*-fdroid-.*\\.apk$"
google: ".*-google-.*\\.apk$"release_source:
url: https://git.mycompany.com/mobile/app
type: gitlabrelease_source:
url: https://example.com/downloads
asset_url: https://cdn\.example\.com/releases/app-v[0-9.]+\.apkrepository: https://github.com/AeonBTC/mempal
commit: a1b2c3d4e5f6Or via CLI:
zsp --commit a1b2c3d4e5f6 zapstore.yamlrepository: naddr1qqxnzd3exsmnjd3exqunjv...zsp apk --extract app.apkOutputs JSON with package ID, version, certificate hash, architectures, permissions, and extracts icon to disk.
MIT