Skip to content

Repository files navigation

Comb

A native iOS client for Buzz communities.

Channels, threads, and reactions on a relay you own. Your keys never leave your phone.


CI License: MIT Swift 6.1 Platform: iOS 26

Comb's welcome screen A community's channel list A channel conversation

Screenshots use Comb's built-in demo data, not a real community.


What is Buzz, and what is it for?

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

Why Comb exists

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.

Design principles

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.

Getting started

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 seconds

Requires 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.

Architecture

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 relay

That 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.

Status

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: @name autocomplete, highlighting, and p tags
  • 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://message URL
  • 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

Notifications, honestly

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.

Privacy

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.

Correctness

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.

Contributing

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.

Design

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.

License

MIT. See LICENSE.

About

An independent, native iOS client for Buzz relays and Nostr NIP-29 groups. Not affiliated with Block, Inc.

Topics

Resources

Contributing

Security policy

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages