A native iOS client for Buzz communities.
Channels, threads, and reactions on a relay you own. Your keys never leave your phone.
Screenshots use Comb's built-in demo data, not a real community.
Every team chat you have used is someone else's server. Slack owns your history and rents it back to you. Discord decides who is allowed in. In both, a bot is a second-class citizen: it has a webhook, not a seat, and the audit trail treats it as something that happened to the conversation rather than something in it.
Buzz is Block's answer to that: a self-hostable workspace where humans and AI agents share the same rooms. Its own description is "a workspace where humans and agents build together, on a relay you own."
The mechanism is the interesting part. Buzz is a Nostr relay, so every message, reaction, workflow step, review approval, and git event is a signed event in one append-only log. Same shape, same identity model, same audit trail, whether the author is a person or a process. An agent that opens a pull request and a human who approves it are the same kind of participant with different keypairs.
Two consequences follow, and they are why a third-party client like Comb can exist at all:
- You own the infrastructure. A Buzz community is a relay at a URL. Run it yourself, or let someone host it. Either way the data is a log you can read without asking permission.
- You own the identity. Membership is a keypair, not a row in a vendor's users table. Any client that can sign an event can participate. Nothing about Buzz requires Buzz's own app.
Comb is the second half of that promise made real on iOS: an app nobody at Block wrote, talking to communities nobody at Comb controls.
Learn more about Buzz: buzz.xyz · github.com/block/buzz
Buzz ships an official mobile app, built in Flutter and shared with Android. It joins by invite, mints its own identity, and covers most of what a phone client needs. Comb is not trying to out-feature it, and this file will not pretend otherwise.
Comb is a native one. Every screen is SwiftUI built on system components, which
is a constraint with consequences: Dynamic Type, VoiceOver, and next year's iOS
design language arrive by rebuilding rather than by reimplementing. An app made
of Form, List, and standard navigation inherits the platform. One made of
hand-drawn chrome inherits the year it was written in.
That is the whole bet. It is a narrower claim than the one this section used to make, and a truer one.
It is also a deliberate proof that the protocol is open enough for somebody else to build a real client on it. Nothing about Buzz requires Buzz's own app, and the way to demonstrate that is to ship one that is not.
Comb is an independent project and is not affiliated with Block, Inc.
These are constraints, not aspirations. Each one rules out implementations that would otherwise be easier.
Relay-agnostic. No relay is hardcoded, privileged, or bundled. A community is a URL the user supplies. Comb has no opinion about who runs it.
Keys stay on the device. Identity is a secp256k1 keypair in the Keychain,
stored ThisDeviceOnly, so it is never copied to iCloud or included in a backup.
Nothing is transmitted anywhere except signed events destined for the relay the
user chose. There is no Comb account, no Comb server, and no analytics.
Standard NIP-29 first, Buzz extensions second. Buzz defines kinds no other
Nostr software understands, such as 40002 rich content and 40003 edits. Comb
renders them when a relay provides them, and every one has a fallback so the app
stays fully usable against a relay that does not. A client that structurally
depends on one vendor's extensions is that vendor's client. See
EventKind.isBuzzExtension.
Verify everything the relay says. Events are checked by recomputing the id from their contents and verifying the signature. Checking only the signature would let a relay swap content while keeping a signature that still verifies over the original id.
git clone https://github.com/jedbridges/comb.git
cd comb
make test # package tests, no simulator, about a second
make run # build, install, and launch on a booted simulator
make test-app # the app target's tests in a simulator, and the dead-UI check
make test-ui # the navigation flows, in a simulator, minutes not secondsRequires Xcode 26 and Swift 6.1 or later. make project regenerates the Xcode
project after changing project.yml; Comb.xcodeproj is generated and is not
checked in.
To try the app without joining anything, launch with --demo for a seeded
in-memory community.
CombCore/ Protocol, crypto, wire format. Foundation only.
CombStore/ The append-only event log, its projections, and the ingest
choke point where verification happens. GRDB.
CombNet/ Relay connection and protocol state machine. Ships a
CombNetTesting product: a transport fixture, and a fake
relay that enforces rules rather than agreeing to
everything.
Comb/ The iOS app target. SwiftUI only.
The load-bearing idea: the event table is the source of truth, append-only, and
everything else is a projection over it. Nostr events are immutable and
content-addressed, so the id is a natural primary key, dedupe is
INSERT OR IGNORE, and derived state can be dropped and rebuilt by replaying the
log. Fixing a projection bug costs a version bump and a local replay, not a
refetch from the relay.
Three of the targets are Swift packages, so make test exercises the protocol,
storage, and relay state machine in about a second with no simulator. That is
where the subtle bugs are.
The rest of the testing is shaped by the two ways this codebase has actually
shipped defects, rather than by coverage as a number. A whole feature once
compiled with nothing to trigger it, so scripts/check-dead-ui.sh looks for
state nothing ever fills in and view properties every caller passes and nobody
reads. A navigation fix once returned without changing the screen, so
make test-ui drives the real navigation stack and asserts where you land.
And because a fake relay that says yes to everything can only ever confirm what
the client already believes, RelayContractTests runs the same cases against
BuzzFake, which enforces NIP-42, filter evaluation and group scoping, and
against a real Buzz relay in Docker when one is running:
export BUZZ_RELAY_PRIVATE_KEY=$(openssl rand -hex 32)
make relay-up # asserts the relay advertises `self`, or refuses to start
make test-live # the same cases, against the fake and the real relayThat assertion is not ceremony. Without a relay key there is no self in
NIP-11, the provenance check on relay-signed group state skips silently, and
the suite passes without testing what it claims to test.
Two protocol details worth knowing if you are reading the code. Edits and deletions are authorised at read time, not at ingest: a kind 5 only erases its own author's events and a kind 40003 only rewrites its author's own, because both arrive validly signed and hosted Buzz's server-side checks do not exist on a plain NIP-29 relay. And images are re-encoded before upload, never passed through, because a photo out of the library carries EXIF that routinely includes GPS.
Working against live Buzz communities. Builds are uploaded to TestFlight and internal testing is running; see docs/TESTFLIGHT.md for the release path.
Zaps are covered by tests that deliberately move no money. Running the paid path once, against a real wallet, is written up in docs/ZAP-TESTING.md.
Protocol and storage
- Hex, bech32, NIP-19
npub/nsec - secp256k1 keys, BIP-340 signing and verification
- NIP-01 events, canonical serialization, id computation, validation
- Filters including dynamic tag filters and p-gated kind detection
- Client and relay wire messages, NIP-42 auth events
- NIP-98 HTTP auth, Blossom BUD-01/02 media auth, signer abstraction
- Append-only event log, verified ingest, rebuildable projections
- Timeline queries with stable pagination, and the optimistic send outbox
- Relay connection, NIP-42 auth, subscriptions, reconnection
The app
- Channel list with unread state, message history, sending
- Cross-device read state, in Comb's own shape and in NIP-RS for other clients
- Onboarding, community discovery with search and sorting
- Keychain storage and the
nostrpair://pairing handshake - Threads, reactions with a full emoji picker, editing and deleting
- Mentions:
@nameautocomplete, highlighting, andptags - Images: Blossom upload, inline rendering, metadata stripped before send
- Search, profiles, member lists, link detection, date separators
- Zaps (NIP-57) by
lightning:deep link, or paid in place over NIP-47 - Sender-attested zaps, so a gated relay can still show a tally
- Spending allowances for agents, decided on your device
- Reporting (NIP-56) and local blocking
- Mention notifications by background refresh, and local reminders
- Deep links to a message, by
buzz://messageURL - Typing indicators
- Direct messages named for the people in them, rather than "dm"
- Haptics on the moments that repeat, and only for what you did
- Presence
- Starting a new direct message
- Creating, joining, and leaving channels
- Video playback for received attachments
- Nostr Wallet Connect (NIP-47) for in-app zaps
There is no real push. Buzz's hosted push gateway verifies Apple App Attest
against a single hardcoded app identifier and its AppProfile is a two-variant
enum, so no third-party client can register with it. Making that configurable is
an upstream change worth proposing, and the payload the gateway sends is a
contentless "reconnect" nudge, so any client could consume it unchanged.
Worth stating plainly rather than implying an unfavourable comparison: no Buzz
client has push today, the official mobile app included. It ships no push
plugin, no aps-environment, and no entitlements file, and the gateway's own
deployment notes say the client enrolment flow is still unbuilt.
What Comb does instead: iOS wakes the app periodically, it syncs, and it posts a local notification for anything that mentioned you. No server is involved. The cost is latency, and the app says so in Settings rather than implying otherwise.
Comb has no backend. It talks to the relay you point it at and nowhere else. No analytics, no crash reporting, no third-party SDKs beyond a local database library. See PRIVACY.md.
One caveat worth stating plainly rather than being caught out on: builds distributed through TestFlight or the App Store have crash reports collected by Apple, gated by your device's own diagnostics setting. That is a property of Apple's distribution, not something Comb opts into, and an app cannot disable it. For diagnosing problems, Comb keeps a local log you can read and send yourself from Settings.
The canonical serializer decides event ids, and an id that disagrees with other
implementations by one byte makes every event Comb produces unverifiable
everywhere else. So the expected ids in CanonicalJSONTests were generated by an
independent Python implementation rather than by this code, covering quotes,
backslashes, control characters, non-BMP emoji, and accented characters. The
bech32 vectors were cross-checked the same way, and the nsec vector is the one
published in the NIP-19 specification.
Contributions are welcome. See CONTRIBUTING.md for the workflow, and SECURITY.md for reporting a vulnerability.
To list a community in Comb's browse screen, no code is required: open a listing request.
Comb borrows Buzz's palette, type scale, radii, and motion tokens so it feels like part of the same world. It does not use Buzz's name or its bee mark: Buzz is Apache 2.0, and section 6 of that license withholds trademark rights. Comb's mark is its own. See DESIGN.md.
MIT. See LICENSE.


