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 Contact List: Follow/unfollow users and manage contact 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-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.6.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,Bech32,SealedMessage,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 onNostrCorebut does not re-export it; add theNostrCoreproduct too and import both.NostrWalletConnect— NIP-47 wallet payments, built onNostrCore. Its API surfaces core types (LNURLPayResponse,Event, …), so add theNostrCoreproduct andimport NostrCorealongside it.NostrConnect— NIP-46 remote signing (bunker://andnostrconnect://flows), built onNostrCore. Its API surfaces core types (Event,KeyPair,UnsignedEvent, …), so add theNostrCoreproduct andimport NostrCorealongside it.
Migrating from an earlier release: the protocol primitives moved out of
NostrClientinto the newNostrCoremodule. Add theNostrCoreproduct to your target andimport NostrCorealongsideimport NostrClientwherever you referenceEvent,KeyPair,EventSigner,Filter,RelayConnection,Bech32,NostrError, and the other primitives — the higher-levelNostrClientAPI (theNostrClientactor,RelayPool, direct messages, outbox) is unchanged.
import NostrClient // the high-level client
import NostrCore // primitives: KeyPair, Event, Filter, EventSigner, …
let keyPair = try KeyPair() // new random keypair
print(try keyPair.npub, try keyPair.nsec)
let imported = try KeyPair(nsec: "nsec1...")
// From a BIP-39 mnemonic (NIP-06)
let (mnemonic, derived) = try KeyPair.generate(wordCount: 12)
print(mnemonic.phrase, try derived.npub)let client = NostrClient()
try await client.connect(to: ["wss://relay.example.com", "wss://relay2.example.com"])
try await client.setNsec("nsec1...")
// Publish a text note — returns the signed event plus the per-relay outcome.
let note = try await client.publishTextNote(content: "Hello, Nostr!")
print("Accepted by \(note.result.acceptedRelays.count) relay(s)")
try await client.publishReaction(to: note.event, content: "🤙")PublishStrategy controls how many acknowledgments a publish waits for (.firstAck, .quorum(n), .allSettled); the returned PublishResult reports the per-relay outcome.
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 timeline = try await client.subscribeToUserTimeline(pubkey: "...")
for await event in timeline.events {
print(event.content)
}
// Custom filter
let filter = Filter(kinds: [1], authors: ["pubkey1"], limit: 100)
for await event in try await client.events(filters: [filter]) {
print(event.id)
}
// One-shot fetch
let metadata = try await client.fetchMetadata(pubkey: "...")
print(metadata?.name ?? "Unknown")Need per-relay EOSE, notices, and auth challenges? Iterate client.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.publishDirectMessageRelayList(relays: ["wss://inbox.example.com"])
try await client.connectDirectMessageInboxRelays()
// Receive — already decrypted and parsed.
for await message in try await client.directMessages() {
print("\(message.senderPubkey): \(message.content)")
}
// Send (encrypted, gift-wrapped, routed to each party's DM relays).
try await client.sendDirectMessage("Hello privately!", to: "recipientPubkeyHex")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
amountMillisats: 21_000,
zapRequest: zapRequest) // signed with EventSigner.signZapRequest(...)
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.
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
import NostrCore
// Start a session from the signer's bunker:// token.
let signer = try RemoteSigner(bunker: try BunkerURI(string: "bunker://..."))
// Prove the user's key and sign an event remotely — the signed event is verified locally.
let userPubkey = try await signer.userPublicKey()
let signed = try await signer.sign(
UnsignedEvent(pubkey: userPubkey, kind: .textNote, content: "Signed by my bunker"))
print(signed.id, try signed.verify())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 pubkey 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)
let remotePubkey = try await signer.awaitConnection() // resolves once the signer acceptsping, NIP-44/NIP-04 encrypt and decrypt, switch_relays, and logout are available too, along with an authChallenges() stream for auth_url approvals.
A RemoteSigner can also drive a NostrClient: NostrClient.setSigner(_:) accepts any NostrSigning, so a remote signer works for sign(_:), publish(_:), and NIP-42 authentication. The convenience publish* and direct-message helpers still require a local key and throw NostrError.localSignerRequired for a remote signer.
try await client.setSigner(signer) // any NostrSigning — a RemoteSigner or a local EventSigner
guard let userPubkey = await client.publicKey else { return }
let signed = try await client.sign(
UnsignedEvent(pubkey: userPubkey, kind: .textNote, content: "Signed by my bunker"))
try await client.publish(signed)Each of these is covered in depth, with worked examples, in the documentation:
- 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 with
subscribeOutbox/publishGossip. - 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.
- Relay information (NIP-11) — fetch a relay's capabilities with
RelayInformation.fetch(fromRelayURLString:). - NIP-19 entities — encode/decode
npub/nsec/note/nprofile/nevent/naddrviaNIP19Entity,NProfile,NEvent, andNAddr. - Low-level APIs — drive a single
RelayConnectiondirectly, or sign events by hand withEventSigner.
- NIP-01: Basic protocol
- NIP-02: Contact 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-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)
- 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)
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