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-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.7.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 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 outbox/gossip 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)import NostrClient
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.relays.connect(to: ["wss://relay.example.com", "wss://relay2.example.com"])
try await client.identity.setNsec("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: "🤙")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.subscriptions.userTimeline(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.subscriptions.events(filters: [filter]) {
print(event.id)
}
// One-shot fetch
let metadata = try await client.events.fetchMetadata(pubkey: "...")
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: ["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() {
print("\(message.senderPubkey): \(message.content)")
}
// Send (encrypted, gift-wrapped, routed to each party's DM relays).
try await client.messages.send("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
// 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: 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
guard let userPubkey = await client.identity.publicKey else { return }
let signed = try await client.identity.sign(
UnsignedEvent(pubkey: userPubkey, kind: .textNote, content: "Signed by my bunker"))
try await client.events.publish(signed)Prove 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 pubkey is the authenticated identity:
let event = try HTTPAuth.validate(
authorization: authorizationHeader, url: requestURL, method: "POST", payload: body)
let authenticatedPubkey = event.pubkeyA 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.timeline(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:
- 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
routing.subscribeOutbox/routing.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.
- HTTP authorization (NIP-98) — sign requests with
URLRequest.setNostrAuthorization(signer:)orHTTPAuth, 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(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-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