
Nostr protocol toolkit: event models, tag builders, secp256k1 signing, NIP‑04/44 encryption, relay client with subscriptions/paging/publish‑and‑confirm, NIP‑45 counts, NIP‑77 sync, SQLite store.
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 21 |
| Android | minSdk 26 |
| Apple | iosArm64, iosSimulatorArm64, macosArm64 (Kotlin), iOS 15 / macOS 12 (Swift) |
| Linux | linuxX64 |
Swift apps get Quartz as the QuartzKit framework. In Xcode, use File > Add Package
Dependencies and enter https://github.com/vitorpamplona/quartz, or add it to Package.swift:
dependencies: [
.package(url: "https://github.com/vitorpamplona/quartz", from: "<version>"),
],
targets: [
.target(name: "MyApp", dependencies: [.product(name: "QuartzKit", package: "quartz")]),
]Kotlin reaches Swift through Objective-C, so a few names change: an extension on a Kotlin class
becomes a method (keyPair.pubKey.toHexKey()), top-level functions and extensions on String
or numbers sit on a class named after their file (HexKeyKt.hexToByteArray("…")), default
arguments must be passed explicitly, and suspend functions become async. The Swift package starts with the first
release after 2.3.0.
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.
fetchAll, fetchFirst, fetchAllPages, publishAndConfirm, count, negentropyReconcile.Manage logged in users with the KeyPair class
val random = KeyPair() // creates a random key
val fromHex = KeyPair("hex...".hexToByteArray())
val fromNsec = KeyPair("nsec1...".bechToBytes())
val fromMnemonic = KeyPair(Nip06().privateKeyFromMnemonic("<mnemonic>"))
val readOnlyFromHex = KeyPair(pubKey = "hex...".hexToByteArray())
val readOnlyFromNpub = 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 (Android only).
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)To answer NIP-42 AUTH challenges, given a logged-in signer. The lambda receives the relay asking,
the template to sign and whether the user may be prompted (interactive):
val authCoordinator =
RelayAuthenticator(client, appScope) { relay, authTemplate, interactive ->
listOf(
// for each signed-in user, return an event
signer.sign(authTemplate),
)
}To make a request subscription simply do:
val metadataSub =
client.subscribe(
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, pass a filters lambda instead. It returns the filters to send to each relay, and is read again every time you refresh the subscription.
val metadataSub =
client.subscribe(
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
}Call metadataSub.refresh() when the users or relays change: it runs the lambda again and updates
the subscription on every relay. Call metadataSub.close() to stop it.
For one-shot work (fetch a set of events and stop, page past a relay's limit, publish and wait for
the relay's OK, NIP-45 counts, NIP-77 sync), use the extensions listed in
One-shot relay operations
instead of writing a subscribe-and-collect loop.
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. JSON is Quartz's own: Kite reads and writes events and relay traffic, and kotlinx.serialization handles everything else. On the JVM and Android, 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, source and binary. Majors come once or
twice a year; between them, an API on its way out keeps working, marked @Deprecated with a
ReplaceWith and the major that removes it. 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.
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 21 |
| Android | minSdk 26 |
| Apple | iosArm64, iosSimulatorArm64, macosArm64 (Kotlin), iOS 15 / macOS 12 (Swift) |
| Linux | linuxX64 |
Swift apps get Quartz as the QuartzKit framework. In Xcode, use File > Add Package
Dependencies and enter https://github.com/vitorpamplona/quartz, or add it to Package.swift:
dependencies: [
.package(url: "https://github.com/vitorpamplona/quartz", from: "<version>"),
],
targets: [
.target(name: "MyApp", dependencies: [.product(name: "QuartzKit", package: "quartz")]),
]Kotlin reaches Swift through Objective-C, so a few names change: an extension on a Kotlin class
becomes a method (keyPair.pubKey.toHexKey()), top-level functions and extensions on String
or numbers sit on a class named after their file (HexKeyKt.hexToByteArray("…")), default
arguments must be passed explicitly, and suspend functions become async. The Swift package starts with the first
release after 2.3.0.
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.
fetchAll, fetchFirst, fetchAllPages, publishAndConfirm, count, negentropyReconcile.Manage logged in users with the KeyPair class
val random = KeyPair() // creates a random key
val fromHex = KeyPair("hex...".hexToByteArray())
val fromNsec = KeyPair("nsec1...".bechToBytes())
val fromMnemonic = KeyPair(Nip06().privateKeyFromMnemonic("<mnemonic>"))
val readOnlyFromHex = KeyPair(pubKey = "hex...".hexToByteArray())
val readOnlyFromNpub = 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 (Android only).
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)To answer NIP-42 AUTH challenges, given a logged-in signer. The lambda receives the relay asking,
the template to sign and whether the user may be prompted (interactive):
val authCoordinator =
RelayAuthenticator(client, appScope) { relay, authTemplate, interactive ->
listOf(
// for each signed-in user, return an event
signer.sign(authTemplate),
)
}To make a request subscription simply do:
val metadataSub =
client.subscribe(
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, pass a filters lambda instead. It returns the filters to send to each relay, and is read again every time you refresh the subscription.
val metadataSub =
client.subscribe(
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
}Call metadataSub.refresh() when the users or relays change: it runs the lambda again and updates
the subscription on every relay. Call metadataSub.close() to stop it.
For one-shot work (fetch a set of events and stop, page past a relay's limit, publish and wait for
the relay's OK, NIP-45 counts, NIP-77 sync), use the extensions listed in
One-shot relay operations
instead of writing a subscribe-and-collect loop.
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. JSON is Quartz's own: Kite reads and writes events and relay traffic, and kotlinx.serialization handles everything else. On the JVM and Android, 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, source and binary. Majors come once or
twice a year; between them, an API on its way out keeps working, marked @Deprecated with a
ReplaceWith and the major that removes it. 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.