Nostr client for Android
$ npx -y skills add vitorpamplona/amethyst --agent claude-code
Run the curl in your terminal, the rest in Claude Code.
What's inside
Join the social network you control.
| OS | CLI install | Direct download |
|---|---|---|
| macOS (Apple Silicon) | brew install --cask amethyst-nostr | .dmg arm64 |
| macOS (Intel) | brew install --cask amethyst-nostr | .dmg x64 |
| Windows 10/11 | winget install VitorPamplona.Amethyst | .msi ยท .zip portable |
| Debian/Ubuntu | โ | .deb |
| Fedora/RHEL/openSUSE | โ | .rpm |
| Any Linux | โ | AppImage ยท .tar.gz |
Coming soon (separate PR): Scoop (Windows), AUR (Arch Linux).
Build from source: see BUILDING.md.
Install troubleshooting (Gatekeeper / SmartScreen / AppImage): see BUILDING.md ยง Troubleshooting installs.
If you sideload Amethyst (Obtainium, GitHub Releases, Zap Store), verify that your
APK is signed by the official release key before installing. All official Amethyst
APKs โ both the googleplay and fdroid flavors โ are signed with the same
certificate, whose SHA-256 fingerprint is:
C2:D0:AA:86:BC:B6:B6:20:90:56:1A:41:BB:E3:36:E9:8B:78:C2:D0:21:0A:49:8D:C8:85:F2:8E:13:48:CF:17
To check a downloaded APK yourself, run (apksigner ships with the Android SDK
build-tools):
apksigner verify --print-certs amethyst-*.apk
and confirm the reported Signer #1 certificate SHA-256 digest is
c2d0aa86bcb6b62090561a41bbe336e98b78c2d0210a498dc885f28e1348cf17.
Without the Android SDK, keytool -printcert -jarfile amethyst-*.apk (bundled
with any JDK) prints the same SHA-256 fingerprint.
With AppVerifier, paste or share the APK and compare against:
com.vitorpamplona.amethyst
C2:D0:AA:86:BC:B6:B6:20:90:56:1A:41:BB:E3:36:E9:8B:78:C2:D0:21:0A:49:8D:C8:85:F2:8E:13:48:CF:17
Relays know your IP address, your name, your location (guessed from IP), your pub key, all your contacts, and other relays, and can read every action you do (post, like, boost, quote, report, etc) except for Private Zaps and Private DMs. While the content of direct messages (DMs) is only visible to you and your DM counterparty, everyone can see when you and your counterparty DM each other.
If you want to improve your privacy, consider utilizing a service that masks your IP address (e.g. a VPN or Tor) from trackers online.
The relay also learns which public keys you are requesting, meaning your public key will be tied to your IP address.
Information shared on Nostr can be re-broadcasted to other servers and should be assumed permanent for privacy purposes. There is no way to guarantee the deletion of any content once posted.
This repository is split between Amethyst, Quartz, Commons, and DesktopApp:
The app architecture consists of the UI, which uses the usual State/ViewModel/Composition, the service layer that connects with Nostr relays, and the model/repository layer, which keeps all Nostr objects in memory, in a full OO graph.
The repository layer stores Nostr Events as Notes and Users separately. Those classes use LiveData and Flow objects to allow the UI and other parts of the app to subscribe to each Note/User and receive updates when they happen. They are also responsible for updating viewModels when needed. As the user scrolls through Events, the Datasource classes are updated to receive more information about those particular Events.
Most of the UI is reactive to changes in the repository classes. The service layer assembles Nostr filters for each need of the app, receives the data from the Relay, and sends it to the repository. Connection with relays is never closed during the use of the app. The UI receives a notification that objects have been updated. Instances of User and Notes are mutable directly. There will never be two Notes with the same ID or two User instances with the same pubkey.
Lastly, the user's account information (private key/pub key) is stored in the Android KeyStore for security.
Make sure to have the following pre-requisites installed:
Fork and clone this repository and import it into Android Studio
git clone https://github.com/vitorpamplona/amethyst.git
Use an Android Studio build action to install and run the app on your device or a simulator.
Build the Android app:
./gradlew assembleDebug
Build and run the Desktop app (requires Java 21+):
./gradlew :desktopApp:run
Full build (including tests)
./gradlew build
Requirements:
brew install libsodium./gradlew test
./gradlew connectedAndroidTest
./gradlew spotlessCheck
./gradlew spotlessApply
For the F-Droid build:
./gradlew installFdroidDebug
For the Play build:
./gradlew installPlayDebug
A release is one tag push. Bump app and appCode in
gradle/libs.versions.toml, then git tag -s vX.Y.Z && git push --tags โ the
Create Release Assets workflow builds and signs every Android, desktop, CLI,
and Maven artifact, and Homebrew + Winget auto-bump on stable tags.
zsp publish, F-Droid
pull, and release-notes publishing.Add Maven Central and Google Maven to your repositories:
repositories {
mavenCentral()
google()
}
Add the following line to your commonMain dependencies:
implementation('com.vitorpamplona.quartz:quartz:1.13.1')
Variations to each platform are also available:
implementation('com.vitorpamplona.quartz:quartz-android:1.13.1')
implementation('com.vitorpamplona.quartz:quartz-jvm:1.13.1')
implementation('com.vitorpamplona.quartz:quartz-iosarm64:1.13.1')
implementation('com.vitorpamplona.quartz:quartz-iossimulatorarm64:1.13.1')
Check versions on MavenCentral
Tagged releases go to Maven Central. For pre-release / snapshot builds โ
e.g. to test an unreleased fix straight from main or a feature branch โ use
JitPack, which builds the module
on demand from any git ref:
repositories {
maven { url = uri("https://jitpack.io") }
}
dependencies {
// version can be a tag, a commit hash, or "<branch>-SNAPSHOT"
implementation("com.github.vitorpamplona.amethyst:quartz:main-SNAPSHOT")
}
The resolvable refs and the exact module coordinates are listed on the JitPack page. Prefer a Maven Central release for anything shipping to production โ JitPack snapshots are not guaranteed stable.
Manage logged in users with the KeyPair class
val keyPair = KeyPair() // creates a random key
val keyPair = KeyPair("hex...".hexToByteArray())
val keyPair = KeyPair("nsec1...".bechToBytes())
val keyPair = KeyPair(Nip06().privateKeyFromMnemonic("<mnemonic>"))
val readOnly = KeyPair(pubKey = "hex...".hexToByteArray())
val readOnly = KeyPair(pubKey = "npub1...".bechToBytes())
Create signers that can be Internal, when you have the private key or a read-only public key, or External, when it is controlled by Amber in NIP-55.
Use either the NostrSignerInternal or NostrSignerExternal class:
val signer = NostrSignerInternal(keyPair)
val amberSigner = NostrSignerExternal(
pubKey = keyPair.pubKey.toHexKey(),
packageName = signerPackageName, // Amber package name
contentResolver = appContext.contentResolver,
)
Create a single NostrClient for the entire application and control which relays it will access by
registering subscriptions and sending events. The pool will automatically change based on filters +
outbox events.
You will need a coroutine scope to process events and if you are using OKHttp, we offer a basic wrapper to create the socket connections themselves.
val appScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
val rootClient = OkHttpClient.Builder().build()
val socketBuilder = BasicOkHttpWebSocket.Builder { url -> rootClient }
val client = NostrClient(socketBuilder, appScope)
If you want to auth, given a logged-in signer:
val authCoordinator = RelayAuthenticator(client, appScope) { authTemplate ->
listOf(
// for each signed-in user, return an event
signer.sign(authTemplate)
)
}
To make a request subscription simply do:
val metadataSub = client.req(
relay = "wss://nos.lol",
filter = Filter(
kinds = listOf(MetadataEvent.KIND),
authors = listOf(signer.pubkey)
)
) { event ->
/* consume event */
}
The client will add the relay to the pool, connect to it and start receiving events. The
metadataSub will be active until you call metadataSub.close(). If the client disconnects
and reconnects, the sub will be active again.
To manage subscriptions that change over time, the simplest approach is to build mutable subscriptions with a filters lambda that you can change at will.
val metadataSub = client.req(
filters = {
// Let's say you have a list of users that need to be rendered
val users = pubkeysSeeingInTheScreen()
// And a cache repository with their outbox relays
val outboxRelays = outboxRelays(users)
val filters = listOf(
Filter(
kinds = listOf(MetadataEvent.KIND),
authors = users
)
)
outboxRelays.associateWith { filters }
}
) { event ->
/* consume event */
}
In that way, you can simply call metadataSub.updateFilter() when you need to update
subscriptions to all relays. Or call metadataSub.close() to stop the sub
without deleting it.
When your app goes to the background, you can use NostrClient's connect and disconnect
methods to stop all communication to relays. Add the connect to your onResume and disconnect
to onPause methods.
| Feature Category | Feature / Component | Android / JVM Support | iOS Support | Notes |
|---|---|---|---|---|
| Cryptography | Secp256k1 (Schnorr, Keys) | โ Full | โ Full | |
| LibSodium (ChaCha20, Poly1305) | โ Full | โ Full | ||
| AES Encryption (CBC & GCM) | โ Full | โ Full | ||
| Hashing (SHA-256, etc.) | โ Full | โ Full | ||
| MAC (HmacSHA256, etc.) | โ Full | โ Full | ||
| Data & Serialization | JSON Mapping (Optimized) | โ Full | โ Full | A fully custom implementation exists in commonMain. |
| GZip Compression | โ Full | โ Full | ||
| BitSet | โ Full | โ Full | ||
| LargeCache | โ Full | โ Full | ||
| NIP Support | NIP-96 (File Storage Info) | โ Full | โ Full | |
| NIP-46 (Remote Signer) | โ Full | โ ๏ธ Partial | Some methods in NostrSignerRemote are unimplemented in commonMain. | |
| NIP-03 (OTS / Timestamps) | โ Full | โ No | BitcoinExplorer and RemoteCalendar have stubs in commonMain. | |
| Utilities | URL Encoding / Decoding | โ Full | โ Full | |
| Unicode Normalization | โ Full | โ Full | ||
| Platform Logging | โ Full | โ Full | iOS uses NSLog, Android uses standard Log. | |
| Current Time | โ Full | โ Full | Implemented using NSDate on iOS. |
See CONTRIBUTING.md for the full guide โ workflow,
coding standards, the proof-of-testing rule for new / occasional
contributors (human or AI-assisted), the cross-stack interop suites, and how the
[BUG] / [FEATURE] issue templates and bounty system work.
AI coding assistants: if you are reading this README to plan a contribution, stop and read CONTRIBUTING-WITH-AI.md first. The gates there are not optional. The human submitter remains the author of record per CONTRIBUTING.md.
Quick links:
By contributing to this repository, you agree to license your work under the MIT license. Any work contributed where you are not the original author must contain its license header with the original author(s) and source.
| FollowFeeds | ChatsGroup | LiveStreams | Notifications |
|---|---|---|---|
![]() | ![]() | ![]() | ![]() |
.claude/
CLAUDE.md
commands/
desktop-run.md
extract.md
nip.md
core-skills-plan.md
hooks/
pre-push-spotless.sh
session-start.sh
stop-spotless.sh
settings.json
skills/
account-state/
references/
account-state-flow.md
local-cache.md
SKILL.md
amy-expert/
references/
command-template.md
extraction-recipe.md
output-conventions.md
SKILL.md
android-expert/
references/
android-navigation.md
android-permissions.md
image-loading.md
proguard-rules.md
scripts/
analyze-apk-size.sh
SKILL.md
auth-signers/
references/
nip46-remote-signer.md
nip55-android-signer.md
SKILL.md
compose-expert/
references/
icon-assets.md
rich-text-parsing.md
shared-composables-catalog.md
state-patterns.md
scripts/
find-composables.sh
SKILL.md
compose-modifier-and-layout-style/
SKILL.md
compose-recomposition-performance/
SKILL.md
compose-side-effects/
SKILL.md
compose-slot-api-pattern/
SKILL.md
compose-stability-diagnostics/
SKILL.md
compose-state-deferred-reads/
SKILL.md
compose-state-holder-ui-split/
SKILL.md
desktop-expert/
references/
desktop-compose-apis.md
desktop-navigation.md
keyboard-shortcuts.md
os-detection.md
SKILL.md
event-store-semantics/
SKILL.md
feed-patterns/
references/
feed-filter-composition.md
viewmodel-base-classes.md
SKILL.md
find-missing-translations/
SKILL.md
find-non-lambda-logs/
SKILL.md
gradle-expert/
references/
build-commands.md
common-errors.md
dependency-graph.md
version-catalog-guide.md
scripts/
analyze-build-time.sh
fix-dependency-conflicts.sh
SKILL.md
kotlin-coroutines/
kotlin-coroutines-structured-concurrency/
SKILL.md
references/
advanced-flow-operators.md
relay-patterns.md
testing-coroutines.md
SKILL.md
kotlin-expert/
references/
common-utilities.md
dsl-builder-examples.md
flow-patterns.md
immutability-patterns.md
sealed-class-catalog.md
SKILL.md
kotlin-flow-state-event-modeling/
SKILL.md
kotlin-multiplatform/
references/
abstraction-examples.md
expect-actual-catalog.md
source-set-hierarchy.md
target-compatibility.md
scripts/
suggest-kmp-dependency.sh
validate-kmp-structure.sh
SKILL.md
kotlin-types-value-class/
SKILL.md
ngit-pr/
SKILL.md
nip85-trusted-assertions/
SKILL.md
nostr-expert/
references/
crypto-and-encryption.md
event-factory.md
event-hierarchy.md
large-cache.md
nip-catalog.md
nip05-identifiers.md
nip19-bech32.md
tag-patterns.md
scripts/
nip-lookup.sh
SKILL.md
quartz-integration/
references/
gradle-setup.md
SKILL.md
relay-client/
references/
filter-assemblers.md
preloaders.md
SKILL.md
searchable-events/
references/
searchable-kinds.md
SKILL.md
.dockerignore
.editorconfig
.git-hooks/
pre-commit
pre-push
.gitattributes
.github/
actions/
assert-stable-release/
action.yml
import-macos-cert/
action.yml
resolve-release/
action.yml
dependabot.yml
ISSUE_TEMPLATE/
bug_report.md
feature_request.md
PULL_REQUEST_TEMPLATE.md
workflows/
build.yml
bump-homebrew-formula.yml
bump-homebrew-geode-formula.yml
bump-homebrew.yml
bump-winget.yml
create-release.yml
crowdin.yml
smoke-test-desktop.yml
.gitignore
.idea/
icon.svg
kotlinc.xml
.spotless/
copyright.kt
amethyst/
amethyst.png
.gitignore
build.gradle.kts
google-services.json
plans/
2026-05-24-ios-support.md
2026-06-20-napplet-inter-applet.md
2026-07-01-auth-permission-architecture.md
2026-07-07-nip29-armada-study.md
2026-07-10-concord-mobile-integration.md
2026-07-12-dual-reply-minichat.md
2026-07-12-relay-ping-interval-study.md
2026-07-12-resource-usage-ledger.md
2026-07-13-chat-scroll-performance.md
2026-07-13-cord06-refounding.md
2026-07-15-bitchat-geohash-interop.md
2026-07-16-location-chats-first-class.md
2026-07-16-nip46-signer-device-checklist.md
2026-07-18-nip29-group-chat-subscriptions.md
2026-07-18-nip29-group-chat-test-plan.md
2026-07-20-v1.13.0-release-qa.md
2026-07-23-push-notification-redesign.md
2026-07-24-nwc-bolt12-pay.md
2026-07-29-location-foreground-gate-impl.md
2026-07-29-location-foreground-gate.md
archive/
2026-05-14-onchain-zaps.md
2026-05-25-appfunctions-signer-prompts.md
2026-05-26-appfunctions-gemini-discovery.md
2026-05-26-appfunctions-screens-as-verbs.md
2026-05-26-avif-implementation-plan.md
2026-05-26-avif-support.md
2026-05-27-avif-instrumented-tests-design.md
2026-05-27-avif-instrumented-tests-plan.md
2026-06-01-dm-live-tail-and-history-slices.md
2026-06-19-napplet-sandbox-host.md
2026-06-20-napplet-ecosystem-audit.md
2026-06-21-napplet-code-audit.md
2026-06-21-napplet-sdk-conformance-audit.md
2026-06-22-napplet-nsite-security.md
2026-06-23-napplet-nap-theme-notify-inc.md
2026-06-24-napplet-embedded-tabs.md
2026-06-25-embed-text-selection-native-parity.md
2026-06-25-web-app-naming.md
2026-06-26-nsite-napplet-favorite-icons.md
README.md
proguard-rules.pro
src/
androidTest/
assets/
avif/
animated-tiny-3frames.avif
still-tiny-8x8-exif-gps.avif
still-tiny-8x8.avif
java/
com/
vitorpamplona/
amethyst/
AvifInstrumentedTestSupport.kt
BdhkeJitCrashTest.kt
CashuBTest.kt
DMFileDecryptionTest.kt
ImageUploadTesting.kt
MentionPreservingInputTransformationTest.kt
NotificationFeedFilterModeOverrideTest.kt
OkHttpOtsTest.kt
service/
images/
AvifAnimatedDecodeInstrumentedTest.kt
ThumbnailDiskCacheAvifInstrumentedTest.kt
ThumbnailDiskCacheInstrumentedTest.kt
playback/
composable/
PlaybackErrorOverlayFitTest.kt
uploads/
AvifMetadataStripperPoisonedFixtureInstrumentedTest.kt
AvifUploadPipelineInstrumentedTest.kt
ThreadDualAxisChartAssemblerTest.kt
tor/
TorBootstrapInstrumentedTest.kt
ui/
components/
AudioPlayerBoxOverflowTest.kt
screen/
loggedIn/
relays/
eventsync/
EventSyncTest.kt
URIParserTest.kt
res/
values/
strings.xml
androidTestPlay/
java/
com/
vitorpamplona/
amethyst/
TranslationsTest.kt
fdroid/
AndroidManifest.xml
java/
com/
vitorpamplona/
amethyst/
service/
ai/
MLKitImageLabelService.kt
NoOpWritingAssistant.kt
WritingAssistantFactory.kt
cast/
chromecast/
ChromecastCaster.kt
lang/
LanguageTranslatorService.kt
notifications/
PushDistributorHandler.kt
PushMessageReceiver.kt
PushNotificationUtils.kt
ui/
components/
SelectNotificationProvider.kt
TranslatableRichTextViewer.kt
screen/
loggedIn/
settings/
LegalSettingsSection.kt
loggedOff/
legal/
TermsGate.kt
main/
AndroidManifest.xml
ic_launcher-playstore.png
java/
com/
vitorpamplona/
amethyst/
Amethyst.kt
AppModules.kt
connectedApps/
consent/
ConnectedAccountRow.kt
SignerConnectActivity.kt
SignerConnectCoordinator.kt
SignerConsentActivity.kt
SignerConsentCoordinator.kt
SignerConsentNotifier.kt
DataStoreNostrSignerPermissionStore.kt
nip46/
DataStoreNip46ClientStore.kt
DebugUtils.kt
EncryptedStorage.kt
favorites/
BrowserHistoryRegistry.kt
BrowserIconRegistry.kt
FavoriteAppLauncher.kt
FavoriteAppsRegistry.kt
FavoriteNostrAppPreloader.kt
NappletFavoriteIcon.kt
LocalPreferences.kt
model/
Account.kt
AccountConcordActions.kt
AccountMarmotActions.kt
AccountRelayGroupActions.kt
accountsCache/
AccountCacheState.kt
AccountSettings.kt
AccountSyncedSettings.kt
AccountSyncedSettingsInternal.kt
AccountZapActions.kt
algoFeeds/
FavoriteAlgoFeedsOrchestrator.kt
AntiSpamFilter.kt
bolt12Offers/
Bolt12OfferListState.kt
CachePruner.kt
CacheSearch.kt
ConcordInviteResult.kt
Dao.kt
edits/
PrivateStorageRelayListDecryptionCache.kt
PrivateStorageRelayListState.kt
EventBroadcaster.kt
GeohashChatIdentityState.kt
GitPullRequestUpdateIndex.kt
GitStatusIndex.kt
HashtagIcon.kt
HomeFeedType.kt
LargeSoftCacheAddressExt.kt
LocalCache.kt
localRelays/
ForwardKind0ToLocalRelayState.kt
LocalRelayListState.kt
marmot/
AndroidKeyPackageBundleStore.kt
AndroidMarmotMessageStore.kt
AndroidMlsGroupStateStore.kt
InMemoryMlsGroupStateStore.kt
KeyPackageRelayListState.kt
MediaAspectRatioCache.kt
MiniFhir.kt
nip01UserMetadata/
AccountHomeRelayState.kt
AccountMineRelayState.kt
AccountOutboxRelayState.kt
NotificationInboxRelayState.kt
UserMetadataState.kt
nip02FollowLists/
DeclaredFollowsPerOutboxRelay.kt
DeclaredFollowsPerUsingRelay.kt
FollowListOutboxOrProxyRelays.kt
FollowListReusedOutboxOrProxyRelays.kt
FollowsPerOutboxRelay.kt
Kind3FollowListState.kt
nip03Timestamp/
BitcoinExplorerEndpoint.kt
... 1600 moreShowing a partial view of a very large repo.
FAQ
amethyst is a Claude Code plugin with 30 hand-picked skills for development work, indexed on Flowy. Install it with the command on its page. It includes account-state, amy-expert, android-expert. Its skills do not fire on their own yet. Request auto-invocation to have Flowy route them as you prompt. Free and open source.