Swift library for Nostr protocol
📖 API documentation — a combined index for all four libraries, with a Getting Started guide, in-depth Advanced Usage, and reference docs for every type.
- Full NIP-01 Support: Events, subscriptions, and relay communication
- NIP-02 Follow List: Follow/unfollow users and manage follow lists
- NIP-03 OpenTimestamps: Attach OTS attestations to events
- NIP-05 Verification: DNS-based identifier verification
- NIP-06 Key Derivation: Generate keys from BIP-39 mnemonic seed phrases
- NIP-17 Private DMs: End-to-end encrypted direct messages with sender anonymity, kind 10050 DM relay routing, reactions, and encrypted file messages
- NIP-40 Expiration: Disappearing messages via an expiration timestamp, including private DMs
- NIP-42 Authentication: Relay AUTH challenges answered automatically, with auth-required retry
- NIP-29 Relay-based Groups: Join, chat in, and moderate groups managed by a single relay —
naddrshare links with invite codes, timeline references, relay-signed state parsing, and the kind-10009 simple group list - NIP-98 HTTP Auth: Sign
Authorization: Nostrheaders for HTTP requests with any signer, and validate them server-side - NIP-57 Zaps: Full Lightning zap flow — sign zap requests (kind 9734), resolve LNURL-pay endpoints, fetch invoices, decode bolt11, and verify kind-9735 zap receipts
- NIP-47 Nostr Wallet Connect: Pay Lightning invoices through a remote wallet over Nostr — the full command set, NIP-44/NIP-04 encryption, notifications, and one-call zap payment (separate
NostrWalletConnectlibrary) - NIP-46 Nostr Connect: Delegate signing to a remote signer that holds the user's key — both the signer-initiated
bunker://and client-initiatednostrconnect://flows,auth_urlchallenges, and typed commands for signing, encryption, and key proof (separateNostrConnectlibrary) - NIP-19 Entities: bech32 encoding/decoding of npub, nsec, note, nprofile, nevent, and naddr
- NIP-49 Private Key Encryption: Password-encrypt a private key to an
ncryptsecstring with scrypt and XChaCha20-Poly1305 - NIP-65 Outbox Model: Per-user read/write relay lists with gossip routing for subscriptions and publishing
- Cryptographic Operations: Schnorr signatures with secp256k1
- Async/Await: Modern Swift concurrency, actor-isolated and fully
Sendable - Multi-Relay Support: Connect to multiple relays with
RelayPool
See the full list of supported NIPs below.
- Swift 6.3+
- iOS 17.0+ / macOS 14.0+ / tvOS 17.0+ / watchOS 10.0+ / visionOS 1.0+
Add to your Package.swift:
dependencies: [
.package(url: "https://github.com/yysskk/swift-nostr", from: "0.10.0")
]Or add via Xcode: File → Add Package Dependencies → Enter the repository URL.
The package vends four libraries:
NostrCore— the shared protocol primitives, cryptography, NIP-19 encoding, and a single-relay WebSocket transport:Event,KeyPair,EventSigner,Filter,NIP44Payload,RelayConnection, and the relay messages. Depend on it directly if that is all you need.NostrClient— the high-level, actor-based client (multi-relay pool, NIP-65 outbox/gossip, NIP-17 direct messages, fetches, NIP-19 entities, zap receipts). It is built onNostrCore, which it re-exports.NostrWalletConnect— NIP-47 wallet payments. Built onNostrCore, which it re-exports.NostrConnect— NIP-46 remote signing (bunker://andnostrconnect://flows). Built onNostrCore, which it re-exports.
NostrClient's API is reached through eight feature namespaces, so completion shows one feature area at a time instead of every operation the client can perform:
| Namespace | What it covers |
|---|---|
client.identity |
The signer, its public key, and NIP-42 relay authentication |
client.relays |
Pool membership, connection lifecycle, and the underlying RelayPool |
client.events |
Publishing, one-time fetches, NIP-23 long-form content |
client.subscriptions |
Live subscriptions as async sequences |
client.routing |
NIP-65 relay lists (behind route: .outbox) and NIP-17 DM relay lists |
client.messages |
NIP-17 private direct messages |
client.groups |
NIP-29 relay-based groups and the kind-10009 group list |
client.lists |
NIP-51 lists and sets |
Each namespace is a Sendable value holding nothing but the client — free to read, store, or pass to a view model — and each conforms to a capability protocol, so a feature depends on the slice it uses instead of the whole client and can be tested against a stub:
struct ChatViewModel {
let messages: any NostrMessaging
}
ChatViewModel(messages: client.messages)A protocol's requirements are only its primitives; the conveniences and their default arguments come from protocol extensions, so a stub implements just the primitives and a call through any NostrMessaging is as short as one through client.messages.
The capability protocols are also the extension points. A helper added to a protocol in an extension works on client.events, on any conformer, and on stubs alike; a namespace of your own is a Sendable struct composing the capabilities it needs, exposed through an extension on NostrClient and testable against stubs. Extending the Client walks through both.
extension NostrEventPublishing {
func publishStatus(_ status: String) async throws -> PublishedEvent { // NIP-38
try await publish(EventTemplate(kind: Event.Kind(rawValue: 30315), tags: [.identifier("general")], content: status))
}
}
struct ZapsAPI: Sendable {
let events: any NostrEventFetching
let identity: any NostrIdentityProviding
}
extension NostrClient {
nonisolated var zaps: ZapsAPI { ZapsAPI(events: events, identity: identity) }
}import NostrClient
let keyPair = KeyPair() // new random keypair
print(keyPair.publicKey.npub, keyPair.privateKey.nsec)
let imported = try KeyPair(nsec: "nsec1...")
// From a BIP-39 mnemonic (NIP-06)
let (mnemonic, derived) = try KeyPair.generate(wordCount: .twelve)
print(mnemonic.phrase, derived.publicKey.npub)let client = NostrClient()
try await client.relays.connect(to: [
RelayURL(string: "wss://relay.example.com"),
RelayURL(string: "wss://relay2.example.com"),
])
try await client.identity.setSigner(EventSigner(nsec: "nsec1..."))
// Publish a text note — returns the signed event plus the per-relay outcome.
let note = try await client.events.publishTextNote(content: "Hello, Nostr!")
print("Accepted by \(note.result.acceptedRelays.count) relay(s)")
try await client.events.publishReaction(to: note.event, content: "🤙")
// Any EventTemplate is signed with the configured signer, then sent along a RelayRoute:
// the whole pool (the default), chosen relays, or the NIP-65 outbox.
let relay2 = try RelayURL(string: "wss://relay2.example.com")
try await client.events.publish(.textNote("Only on relay 2"), route: .relays([relay2]))A RelayURL is a validated ws/wss URL in canonical form (lowercased scheme and host, no root slash or default port), so every spelling of a relay names the same pool entry; RelayURL(string:) throws a RelayURL.Error naming why anything else is rejected.
PublishStrategy controls how many acknowledgments a publish waits for (.firstAck, .quorum(n), .allSettled); the returned PublishResult reports the per-relay outcome. A relay that refuses the event reports a RelayRejection, whose prefix (.pow, .rateLimited, .authRequired, …) says why.
Subscriptions are async sequences — iterate them with for await. The subscription closes automatically when the loop ends or its task is cancelled.
// Live timeline
let alice = try PublicKey(npub: "npub1...")
let timeline = try await client.subscriptions.subscribe(filters: [.userNotes(publicKey: alice, limit: 100)])
for await event in timeline.events {
print(event.content)
}
// Custom filter
let filter = Filter(authors: [alice], kinds: [1], limit: 100)
for await event in try await client.subscriptions.subscribe(filters: [filter]).events {
print(event.id)
}
// One-shot fetch
let metadata = try await client.events.fetchMetadata(for: alice)
print(metadata?.name ?? "Unknown")Need per-relay EOSE, notices, and auth challenges? Iterate client.subscriptions.subscribe(filters:) directly — see Advanced Usage.
// Advertise where you receive DMs (kind 10050; NIP-17 suggests 1–3 relays), then connect your inbox.
try await client.routing.publishDirectMessageRelayList(relays: [try RelayURL(string: "wss://inbox.example.com")])
try await client.routing.connectDirectMessageInboxRelays()
// Receive — already decrypted and parsed. Wraps meant for someone else are skipped;
// a signer that cannot be reached throws, so a failure is never mistaken for silence.
for try await message in try await client.messages.subscribe().messages {
print("\(message.senderPublicKey): \(message.content)")
}
// Send (encrypted, gift-wrapped, routed to each party's DM relays).
try await client.messages.send("Hello privately!", to: alice)
// Any other rumor kind, wrapped and routed the same way.
try await client.messages.sendGiftWrapped(EventTemplate(kind: Event.Kind(rawValue: 30_999), content: payload), to: alice)Reactions (NIP-25), encrypted file messages (kind 15), and disappearing messages (NIP-40) build on the same flow — see Advanced Usage.
The NostrWalletConnect library pays Lightning invoices through a remote wallet, completing the zap flow that NostrClient can only prepare. See its API documentation — a Getting Started guide and in-depth Advanced Usage.
import NostrWalletConnect
// Connect with the wallet's nostr+walletconnect:// string.
let connection = WalletConnection(uri: try WalletConnectURI(string: "nostr+walletconnect://..."))
// Pay any invoice and get the preimage back.
let payment = try await connection.payInvoice("lnbc...")
print(payment.preimage)
// Or complete a zap end to end: fetch the recipient's invoice and pay it.
let zap = try await connection.payZap(
lnurlPay: lnurlPay, // resolved LNURLPayResponse
amount: 21_000,
zapRequest: zapRequest) // try await signer.sign(.zapRequest(to: recipient, relays: relays))
print(zap.preimage)get_balance, get_info, make_invoice, lookup_invoice, list_transactions, keysend, and multi-payments are available too, along with a notifications() stream (call connect() or send any command to start receiving); send(_:) sends any other single-response command defined as a WalletConnectRequest.
The NostrConnect library delegates signing to a remote signer that holds the user's key, so the app never handles an nsec. It supports both the signer-initiated bunker:// flow and the client-initiated nostrconnect:// flow. See its API documentation and Getting Started guide.
import NostrConnect
// Start a session from the signer's bunker:// token.
let signer = RemoteSigner(bunker: try BunkerURI(string: "bunker://..."))
// The first command connects and presents the token's secret.
// Prove the user's key and sign an event remotely — the signed event is verified locally.
let userPublicKey = try await signer.publicKey
let signed = try await signer.sign(.textNote("Signed by my bunker"))
print(signed.publicKey == userPublicKey, signed.isValid)Or start the handshake from the client: generate a nostrconnect:// invitation, show it as a QR code or link, and wait for the signer to accept — its public key is discovered from the response, which is validated against the invitation secret.
let invitation = try NostrConnectURI.invitation(clientKeyPair: clientKeyPair, relays: [relayURL])
displayQRCode(invitation.stringValue)
let signer = try RemoteSigner(invitation: invitation, clientKeyPair: clientKeyPair)
try await signer.connect() // returns once the signer accepts
let remotePublicKey = await signer.remoteSignerPublicKey // the signer's PublicKey, set by connect()To resume a bunker:// session with a persisted client key pair, whose single-use secret was already spent, set RemoteSigner.Config.connectsAutomatically to false.
ping, NIP-44/NIP-04 encrypt and decrypt, switch_relays, and logout are available too, along with an authChallenges() stream for auth_url approvals. fetchPreferredRelays() asks the signer for its preferred relays (switch_relays); reconnecting on them is up to the caller. RemoteSignerMethod is an open set, so send(_:params:) calls a method without a typed wrapper and returns its raw result.
A RemoteSigner can also drive a NostrClient: client.identity.setSigner(_:) accepts any NostrSigning, and every feature runs through that abstraction — identity.sign(_:), events.publish(_:), the convenience publish* helpers, NIP-17 direct messages, NIP-51 private list items, and NIP-42 authentication all work with a remote signer exactly as they do with a local key.
try await client.identity.setSigner(signer) // any NostrSigning — a RemoteSigner or a local EventSigner
try await client.events.publish(.textNote("Signed by my bunker")) // signs the template, then publishes itProve your Nostr identity to an HTTP server with a signed kind-27235 event in the Authorization header — no account or session needed. Sign the final form of the request; the event commits to its exact URL, method, and body:
import NostrCore
var request = URLRequest(url: URL(string: "https://api.example.com/upload")!)
request.httpMethod = "POST"
request.httpBody = body
try await request.setNostrAuthorization(signer: signer) // any NostrSigningOn the receiving side, HTTPAuth.validate runs the full NIP-98 check chain — scheme, decoding, kind, signature, timestamp window, URL, method, body hash — and returns the verified event, whose publicKey is the authenticated identity:
do {
let event = try HTTPAuth.validate(
authorization: authorizationHeader, url: requestURL, method: "POST", payload: body)
let authenticatedPublicKey = event.publicKey
} catch .timestampOutOfTolerance {
// Stale or clock-skewed credential: ask the client to sign a fresh one.
} catch {
// `error` is an `HTTPAuth.ValidationError` naming the failed check.
}A NIP-29 group lives on a single relay, which enforces membership and permissions; every group flow targets exactly that relay. Groups are shared as the naddr of their kind-39000 metadata event, optionally carrying an invite code:
import NostrClient
// Parse a share link and join (kind 9021) — the invite code is applied automatically.
let group = try GroupReference(naddrString: "naddr1...?invite=A7fjq2")
try await client.groups.join(group)
// Follow the live timeline, keeping recent events for timeline references.
var recent: [Event] = []
let timeline = try await client.groups.subscribe(to: group)
for await event in timeline.events {
recent.append(event)
print(event.content)
}
// Chat (NIP-C7 kind 9 by default), referencing recent events so rebroadcasts are detectable.
try await client.groups.publishMessage(
"Hello, group!",
in: group,
previous: Groups.previousReferences(
from: recent, excludingAuthor: await client.identity.publicKey)
)Private groups require NIP-42 AUTH, which the client answers automatically once a signer is set — no extra code.
Each of these is covered in depth, with worked examples, in the documentation:
- Subscriptions — every subscription sequence conforms to
RelaySubscription, so[any RelaySubscription]tracks and closes subscriptions of different element types together. - Encrypted file messages (NIP-17) — encrypt with
EncryptedFile, describe the upload withFileAttachment, and send it withclient.messages.sendFile(_:to:). - Encryption (NIP-44) —
NIP44Payloadencrypts with a key pair or aConversationKey, such as one shared by a group. - Internet identifiers (NIP-05) — parse
name@domainwithInternetIdentifierand check it withverify();UserMetadata.internetIdentifierparses a profile'snip05. - Lightning Zaps (NIP-57) — resolve an LNURL-pay endpoint, sign a zap request, fetch the bolt11 invoice, and verify the kind-9735 receipt.
- Outbox model (NIP-65) — publish your read/write relay list and route reads/writes to each user's declared relays by passing
route: .outboxto a publish, fetch, or subscription. - Client authentication (NIP-42) — AUTH challenges are answered automatically once a signer is set, with auth-required publish retry; an opt-in manual mode is available.
- HTTP authorization (NIP-98) — sign requests with
URLRequest.setNostrAuthorization(signer:)orEventTemplate.httpAuthorization(url:method:payload:), and validate incoming headers withHTTPAuth.validate. - Relay-based groups (NIP-29) — parse
naddrshare links withGroupReference, join and leave, chat with timeline references, moderate withGroups.ModerationAction, fetch validated relay-signed state withgroups.fetchState, and keep the kind-10009 simple group list fresh. - Relay information (NIP-11) — fetch a relay's capabilities with
RelayInformation.fetch(from:). - NIP-19 entities — encode/decode
npub/nsec/note/nprofile/nevent/naddrviaNIP19Entity,NProfile,NEvent, andNAddr. - Low-level APIs — drive a single
RelayConnectiondirectly, or build anEventTemplate(.textNote,.reaction,.zapRequest, …) and sign it —signer.sign(.textNote("gm"))with a localEventSigner, ortry awaitwith anyNostrSigning, such as a remote signer.
- NIP-01: Basic protocol
- NIP-02: Follow list and petnames
- NIP-03: OpenTimestamps attestations
- NIP-05: DNS-based identifiers
- NIP-06: Basic key derivation from mnemonic seed phrase
- NIP-09: Event deletion
- NIP-10: Reply threading (root/reply markers)
- NIP-11: Relay information document
- NIP-13: Proof of Work
- NIP-17: Private direct messages (kind 10050 DM relay lists, encrypted kind 15 file messages)
- NIP-18: Reposts
- NIP-19: bech32-encoded entities (npub, nsec, note, nprofile, nevent, naddr)
- NIP-20: Command Results (OK)
- NIP-21: nostr: URI scheme
- NIP-23: Long-form content (kind 30023 articles, kind 30024 drafts)
- NIP-25: Reactions (incl. gift-wrapped private DM reactions)
- NIP-27: Text note references
- NIP-29: Relay-based Groups (join/leave, chat, moderation kinds 9000-9002, 9005, 9007-9010, relay-signed state parsing, naddr share links with invite codes; LiveKit AV rooms are not modeled — only the kind-39000
livekitflag is parsed) - NIP-40: Expiration timestamp (disappearing messages)
- NIP-42: Client authentication (automatic challenge response, auth-required retry)
- NIP-44: Versioned encryption
- NIP-45: Event counts (COUNT)
- NIP-46: Nostr Connect (remote signing; bunker:// and nostrconnect:// — separate NostrConnect library)
- NIP-47: Nostr Wallet Connect (full command set, NIP-44/NIP-04 encryption, notifications, end-to-end zap payment — separate
NostrWalletConnectlibrary) - NIP-49: Private key encryption (ncryptsec)
- NIP-50: Search capability
- NIP-51: Lists (standard lists + parameterized sets; NIP-44-encrypted private items; kind-10009 simple group list)
- NIP-56: Reporting (kind 1984)
- NIP-57: Lightning Zaps (zap request kind 9734, LNURL helpers, invoice fetch, bolt11 decoding, kind-9735 receipt validation)
- NIP-59: Gift wrap
- NIP-65: Relay list metadata (outbox model)
- NIP-98: HTTP Auth (kind-27235 authorization events, header encoding, server-side validation)
This project uses swift-format (bundled with the Swift toolchain) for code formatting. The configuration lives in .swift-format and is enforced in CI.
# Format the code in place
swift format --in-place --recursive --parallel Sources Tests Package.swift
# Check formatting without modifying files (matches CI)
swift format lint --strict --recursive --parallel Sources Tests Package.swiftContributions are welcome! See the contributing guide for how to build, test, and submit changes, the changelog for release history, the code of conduct for community expectations, and the security policy for how to report vulnerabilities.
MIT License