Why not NIP 29?
This week I finally released Flotilla, a discord/slack-like client I’ve been working on for about three months. This project began as a NIP 29 client, and after 3 PRs, lots of discussion, a podcast, and a partial implementation, I decided to go my own way.
This of course means that I broke compatibility with all the NIP 29 group clients out there, but I did it for good reasons. In this post I hope to explain those reasons, and speculate on how best to move forward with “relay-based” groups on nostr.
To give you some quick context, NIP 29 and my approach to groups (which I’ll dub “relays-as-groups” for clarity) are very similar, with a fundamental difference. Both have chat, join requests, group metadata, membership, moderation, etc. However, the basic unit of a NIP 29 group is a random group id string, while the basic unit of a Flotilla group is a relay itself.
I believe this design difference emerged in part because of what we were attempting to build. NIP 29 groups tend to be more telegram-like, where groups function more like chat rooms, and users join each one individually. On flotilla, groups function more like Discord servers, or Slack workspaces, and users join an entire group of chat rooms at once.
“Relays as groups” has four major advantages over “groups hosted by relays”.
Decentralization
First, using relays as groups supports decentralization better than hosting user-managed groups on relays.
(To be clear here, I’m not referring to client-managed groups, which is a whole different approach that we’ve experimented with in the past, both with NIP 72 communities and with NIP 87 encrypted groups. Both NIP 29 and relays-as-groups solve many of the consistency problems associated with attempting to have a linear, synchronous conversation across multiple relays. While both alternatives have a story for migrating or mirroring a group, both rely heavily on the host relay to not censor user messages.)
I know what you’re thinking. How can reducing the number of relays improve decentralization? Relays were originally introduced in order to create redundancy and spread trust across many actors, creating censorship resistance. This model was difficult for many bitcoiners to wrap their heads around, because it’s a very different kind of decentralization than what a blockchain delivers.
Communities are an essentially different use case from a broadcast network where content is delivered based on author or recipient relay selections. Every message to a community would have to be delivered to all members of the community. Sending each message to all members’ personal inbox relays just doesn’t make sense; there has to be some other inbox for the community to work off of.
Additionally, online communities almost always have moderators and admins. This is even true of very public, open types of communities, like subreddits. The only type of community that doesn’t have moderators is one that emerges naturally from social clustering. And even in that case there is loose consensus about who is in and who isn’t, based on the actions/follows/mutes of the participants, whether these clusters are huge or tiny. Socially-emergent groups are served well by chat applications or broadcast networks.
But the subset of online communities that do prefer to confer moderator status on certain members are essentially centralized. In other words, centralization and control is a feature, not a bug.
Now, that doesn’t mean there don’t need to be considerations around credible exit and removing/adding moderators over time. But the fact is that moderator-led communities are always under the oversight of the moderators at any given time, even if the identity of those moderators changes and their power is limited.
What this implies is that decentralization for moderator-led groups looks very different from decentralization for a broadcast network. There is nothing at all wrong with giving moderators full control over the group’s communications (qua the group; DMs and public broadcast content between group members should happen outside the group’s infrastructure, just as people also exist outside the communities they are a part of). What is important is that no one has control over groups that they aren’t nominally the admin of.
In concrete terms, what this all means is that community moderators should self-host their infrastructure. This is the same principle as motivates self-custody and home servers, but applied to communities. If community leaders manage their own relays, this means that no hosting company or relay admin can de-platform their community. Centralization of network infrastructure in this case aligns with the trust structure of the group.
Applying this to our group dilemma, it’s easy to see that NIP 29 groups are more vulnerable to censorship or data harvesting attacks by malicious relay admins, since many unrelated groups might live on a single relay. In contrast, if you treat relays as groups themselves, every group is forced to live on a separate relay, spreading risk across more hosting providers.
Now, this doesn’t necessarily mean that many “relays” aren’t “virtual relays” managed by the same hosting provider. So I’ll admit that even “relay-based” groups don’t completely solve this problem. But I think it will tend to nudge community organizers toward thinking about community infrastructure in a more self-sovereign (or community-sovereign) way.
Investment in Relays
While both NIP 29 and relays-as-groups rely heavily on relays to implement the features that support each specification, there’s an important difference between the feature sets. In NIP 29, relay support is specific only to groups, and isn’t applicable to other use cases. In contrast, every protocol feature added to support the “relays as groups” can be re-purposed for other types of relays.
Take join requests for example. NIP 29’s kind 9021
events allow users to request access to a group, and that’s all. Kind 28934
join requests on the other hand allow users to request access to relays. Which in the relays-as-groups model means group access, but it also means custom feed access, inbox relay access, maybe even blossom server access. In fact, kind 28934
was originally proposed at the beginning of this year in order to support a different version of hosted groups, but remains as relevant as it ever was despite iteration on groups.
The orthogonality of features added to relays to any specific use case will long-term result in simpler specs, and more interesting relay-based use cases being possible. Join requests are only one example. The same is true of 1984-based moderation, the proposed LIMITS command, AUTH, NIP 11 relay metadata, etc.
We already have web of trust relays, feed relays, archival relays, and many more. Being able to request access to closed versions of these is useful. Being able to signal federation between multiple instances of these, run by different people, is useful. And of course, relay metadata, reports, and LIMITS are self-evidently useful for normal relays, since they pre-date Flotilla.
I’ve always said that relays are some of the coolest and most under-appreciated parts of nostr. This doesn’t mean that we should add every possible feature to them, but features related to data curation and access control fit really well with what relays are good for. For more on the role of relays and what features should be added to them, see my nostrasia talk Functional Relays.
Declarative vs Imperative
A common paradigm in programming is that of declarative vs imperative programming. Imperative programming focuses on “how” to achieve a given result, leaving “what” the code is doing to be inferred by the programmer. Declarative programming instead focuses on the “what”, and allows some underlying implementation to solve the how. A good balance between these paradigms (and knowing when to use one over the other) allows programmers to work faster, make fewer mistakes, and produce less code.
Another way to look at this is that a specification should contain as much ambiguity as possible, but without compromising the system attributes the specification is supposed to guarantee. It can get complex when figuring out what attributes are core to the specification, since sometimes the “how” does actually matter a lot.
However, nostr in particular falls pretty far along the “declarative” end of this spectrum because of its decentralized nature. The only person who can say anything with any authority is the person who signs an event. This event is a “declaration”, and any effects it has are necessarily up to the relays, clients, and people interpreting the event. However, what others do with an event is an expectation that must be taken into account by the publisher, forming a feedback loop. This dialectic is what creates stability in the protocol.
In more concrete terms, no one can “tell” anyone else what they have to do by publishing an event like you might in a traditional, centralized RPC-type system. Any event whose semantics are a “command” rather than a “fact” or “request” is broken unless the counter party is fully committed to carrying out the command. An example of a “command” scenario on nostr is NIP 46 remote signing, in which the bunker is the agent of the user making the request. If the bunker implementation fails to carry out a valid command initiated by the user, its interpretation of that event is objectively incorrect.
NIP 29 applies this same paradigm to relays, particularly in the area of moderation, membership edits, and group metadata. In other words, there are several “commands” which instruct the relay to do something.
This isn’t necessarily a bad thing, but it does increase the number of things the interface between the client and relay have to agree on. A regular relay may accept an add-user
request, but then do nothing with it, violating the contract it has implicitly accepted with the user. The solution to this is feature detection, which is a whole other API to be specified and implemented.
My ideal solution to this problem is to shift the semantics of events away from “commands” to “facts” - in other words, to make the interface more declarative.
In fact, we already have an interface for moderation that works like this. Many clients support kind 1984
“report” events. Users sending these reports have no expectations about how they will be used. They are a “fact”, a declaration of opinion with certain semantics. Other actors in the network may choose whether or not to pay attention to these.
This same model is easily applied to communities. Without having to implement any feature detection (either for the relay’s implementation, or for the user’s role on that relay), anyone can simply send a “report”. This goes into the black hole of the relay, and may subsequently be ignored, broadcasted, or acted on.
The really nice thing about this model is that because there is no expectation for “how” reports are to be interpreted, any approach to moderation can be used depending on relay policy or client implementation. In NIP 29, if you issue a delete-event
, it either happens or it doesn’t and if it doesn’t, you have to explain the failure to the user somehow.
In the relays-as-groups model, e-tagging an event in a kind 1984
requires no user feedback, and therefore it can be interpreted however the relay prefer. This can result in insta-banning, manual review, thresholds based on number of reporters, a leaky-bucket social score algorithm, shadow banning, temporary banning, soft-moderation by allowing clients to request reports and respond to them by changing user interface elements, or anything else you can think of.
The reason I think this is important is that community moderation is a very hard problem, and baking certain semantics into the specification can result in the complete failure of the spec. NIP 72 should be considered an example of what not to do. Some NIP 72 communities have survived due to the dedication of the moderators, but many more have failed because of the rigid moderation model. We should try not to make the same mistake again.
Conclusion
Now, having said all that, I think there is actually a lot of value to NIP 29. What finally clicked for me this week after releasing Flotilla is that the two approaches are actually complementary to one another. One of the most common feature requests I’ve already heard for flotilla is to have more complete support for rooms, which are currently implemented as not much more than hashtags. Better rooms (i.e., “nested groups”) would require: authentication, membership, moderation, and pretty much everything else that exists for the top-level group.
As much as I believe the relays-as-groups approach is superior to NIP 29 for top-level groups, it doesn’t make any sense to try to “nest” relays to create sub-groups. Something like NIP 29 is needed in order to fully support rooms anyway, so I think the convergence of the two approaches is all but inevitable. In fact, fiatjaf has already merged a PR which will allow me to use the same event kinds in flotilla as exist already in NIP 29 clients.
There are just a few more changes that are necessary in order for me to fully adopt NIP 29 in Flotilla:
- NIP 29 feature detection
- Opaque ids for unmanaged groups prevent unmanaged groups from having human-readable names
- We need a mechanism for building membership lists without relay support
- Better handling for
9021
group join requests
I’ve opened PRs for each of these (linked above). Hopefully we can work through these issues and combine our powers to become the Captain Planet of group implementations.