A Nostr library for Kotlin Multiplatform: JVM, Android, iOS, macOS and Linux. Quartz has the event classes, tags and builders for the NIPs, signing (secp256k1) and NIP-04/NIP-44 encryption, a relay client with subscriptions, paging, publish-and-confirm, NIP-45 counts and NIP-77 sync, a SQLite event store with NIP-50 search, and the server side to run a relay on. It also carries whole protocol families built on Nostr: Marmot's events (MLS over Nostr), ContextVM, Concord, OpenAgents and Buzz. The MLS engines live in their own libraries on top of Quartz: marmot-kmp (Marmot groups) and cordn-kmp (cordn).
It is the protocol layer of Amethyst, its desktop
app, the amy CLI and the geode relay.
dependencies {
implementation("com.vitorpamplona.quartz:quartz:<version>")
}| Requirement | |
|---|---|
| JVM | Java 17 |
| Android | minSdk 26 |
| Apple | iosArm64, iosSimulatorArm64, macosArm64 |
| Linux | linuxX64 |
Tagged releases go to Maven Central. To test an unreleased fix straight from main or a branch, use
JitPack, which builds the library 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.quartz:quartz:main-SNAPSHOT")
}Prefer a Maven Central release for anything shipping to production; JitPack snapshots are not guaranteed stable.
- Client architecture: wiring the store, cache, client and signer into an app.
- Client API: transports, fetching, subscribing and publishing.
- Relay: running a relay on Quartz's server code.
- One-shot relay operations:
fetchAll,fetchFirst,fetchAllPages,publishAndConfirm,count,negentropyReconcile. - Agent skill: install it in your project so a coding agent reaches for Quartz's helpers instead of re-implementing them.
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.
Crypto primitives come from crypto-kmp, secp256k1 from secp256k1-kmp and NIP-77 from negentropy-kmp. On the JVM and Android, Jackson parses events and OkHttp is the bundled WebSocket transport.
./gradlew installGitHook # pre-commit (spotless) and pre-push (checks + tests) hooks
./gradlew spotlessApply
./gradlew :quartz:jvmTest
./gradlew :quartz:verifyKmpPurity :quartz:compileCommonMainKotlinMetadata :quartz:compileAppleMainKotlinMetadata
./gradlew :quartz:linuxX64Test # the whole suite on Linux Native
./gradlew :quartz:iosSimulatorArm64Test # macOS onlyTo work on crypto-kmp alongside Quartz, point Gradle at a local checkout:
./gradlew -PcryptoKmpDir=../crypto-kmp :quartz:jvmTest. To try a Quartz
change in Amethyst, build Amethyst with -PquartzDir=../quartz.
AGENTS.md has the project rules, for people and coding agents alike.
Releases follow semantic versioning: a breaking change to the public API
bumps the major version. Each release is tagged v<version> and listed on the
releases page. Until 1.19.0 Quartz was
released from the Amethyst repo with the app's version number.
MIT, see LICENSE.