hodlbod on Nostr: For anyone out there desirous of implementing a Flotilla- (and Chachi-) compatible ...
For anyone out there desirous of implementing a Flotilla- (and Chachi-) compatible client:
quoting nevent1q…dcv9When developing on nostr, normally it's enough to read the NIP related to a given feature you want to build to know what has to be done. But there are some aspects of nostr development that aren't so straightforward because they depend less on specific data formats than on how different concepts are combined.
An example of this is how for a while it was considered best practice to re-publish notes when replying to them. This practice emerged before the outbox model gained traction, and was a hacky way of attempting to ensure relays had the full context required for a given note. Over time though, pubkey hints emerged as a better way to ensure other clients could find required context.
Another one of these things is "relay-based groups", or as I prefer to call it "relays-as-groups" (RAG). Such a thing doesn't really exist - there's no spec for it (although some _aspects_ of the concept are included in NIP 29), but at the same time there are two concrete implementations (Flotilla and Chachi) which leverage several different NIPs in order to create a cohesive system for groups on nostr.
This composability is one of the neat qualities of nostr. Not only would it be unhelpful to specify how different parts of the protocol should work together, it would be impossible because of the number of possible combinations possible just from applying a little bit of common sense to the NIPs repo. No one said it was ok to put `t` tags on a `kind 0`. But no one's stopping you! And the semantics are basically self-evident if you understand its component parts.
So, instead of writing a NIP that sets relay-based groups in stone, I'm writing this guide in order to document how I've combined different parts of the nostr protocol to create a compelling architecture for groups.
## Relays
Relays already have a canonical identity, which is the relay's url. Events posted to a relay can be thought of as "posted to that group". This means that every relay is already a group. All nostr notes have already been posted to one or more groups.
One common objection to this structure is that identifying a group with a relay means that groups are dependent on the relay to continue hosting the group. In normal broadcast nostr (which forms organic permissionless groups based on user-centric social clustering), this is a very bad thing, because hosts are orthogonal to group identity. Communities are completely different. Communities actually need someone to enforce community boundaries, implement moderation, etc. Reliance on a host is a feature, not a bug (in contrast to NIP 29 groups, which tend to co-locate many groups on a single host, relays-as-groups tends to encourage one group, one host).
This doesn't mean that federation, mirrors, and migration can't be accomplished. In a sense, leaving this on the social layer is a good thing, because it adds friction to the dissolution/forking of a group. But the door is wide open to protocol additions to support those use cases for relay-based groups. One possible approach would be to follow [this draft PR](https://github.com/coracle-social/nips/blob/60179dfba2a51479c569c9192290bb4cefc660a8/xx.md#federation) which specified a "federation" event relays could publish on their own behalf.
## Relay keys
[This draft PR to NIP 11](https://github.com/nostr-protocol/nips/pull/1764) specifies a `self` field which represents the relay's identity. Using this, relays can publish events on their own behalf. Currently, the `pubkey` field sort of does the same thing, but is overloaded as a contact field for the owner of the relay.
## AUTH
Relays can control access using [NIP 42 AUTH](https://github.com/nostr-protocol/nips/blob/master/42.md). There are any number of modes a relay can operate in:
1. No auth, fully public - anyone can read/write to the group.
2. Relays may enforce broad or granular access controls with AUTH.
Relays may deny EVENTs or REQs depending on user identity. Messages returned in AUTH, CLOSED, or OK messages should be human readable. It's crucial that clients show these error messages to users. Here's how Flotilla handles failed AUTH and denied event publishing:
![Demo]()
[LIMITS](https://github.com/nostr-protocol/nips/pull/1434) could also be used in theory to help clients adapt their interface depending on user abilities and relay policy.
3. AUTH with implicit access controls.
In this mode, relays may exclude matching events from REQs if the user does not have permission to view them. This can be useful for multi-use relays that host hidden rooms. This mode should be used with caution, because it can result in confusion for the end user.
See [Triflector](https://github.com/coracle-social/triflector) for a relay implementation that supports some of these auth policies.
## Invite codes
If a user doesn't have access to a relay, they can request access using [this draft NIP](https://github.com/nostr-protocol/nips/pull/1079). This is true whether access has been explicitly or implicitly denied (although users will have to know that they should use an invite code to request access).
The above referenced NIP also contains a mechanism for users to request an invite code that they can share with other users.
The policy for these invite codes is entirely up to the relay. They may be single-use, multi-use, or require additional verification. Additional requirements can be communicated to the user in the OK message, for example directions to visit an external URL to register.
See [Triflector](https://github.com/coracle-social/triflector) for a relay implementation that supports invite codes.
## Content
Any kind of event can be published to a relay being treated as a group, unless rejected by the relay implementation. In particular, [NIP 7D](https://github.com/nostr-protocol/nips/blob/master/7D.md) was added to support basic threads, and [NIP C7](https://github.com/nostr-protocol/nips/blob/master/C7.md) for chat messages.
Since which relay an event came from determines which group it was posted to, clients need to have a mechanism for keeping track of which relay they received an event from, and should not broadcast events to other relays (unless intending to cross-post the content).
## Rooms
Rooms follow [NIP 29](https://github.com/nostr-protocol/nips/blob/master/29.md). I wish NIP 29 wasn't called "relay based groups", which is very confusing when talking about "relays as groups". It's much better to think of them as sub-groups, or as Flotilla calls them, "rooms".
Rooms have two modes - managed and unmanaged. Managed rooms follow all the rules laid out in NIP 29 about metadata published by the relay and user membership. In either case, rooms are represented by a random room id, and are posted to by including the id in an event's `h` tag. This allows rooms to switch between managed and unmanaged modes without losing any content.
Managed room names come from `kind 39000` room meta events, but unmanaged rooms don't have these. Instead, room names should come from members' NIP 51 `kind 10009` membership lists. Tags on these lists should look like this: `["group", "groupid", "wss://group.example.com", "Cat lovers"]`. If no name can be found for the room (i.e., there aren't any members), the room should be ignored by clients.
Rooms present a difficulty for publishing to the relay as a whole, since content with an `h` tag can't be excluded from requests. Currently, relay-wide posts are h-tagged with `_` which works for "group" clients, but not more generally. I'm not sure how to solve this other than to ask relays to support negative filters.
## Cross-posting
The simplest way to cross-post content from one group (or room) to another, is to quote the original note in whatever event kind is appropriate. For example, a blog post might be quoted in a `kind 9` to be cross-posted to chat, or in a `kind 11` to be cross-posted to a thread. `kind 16` reposts can be used the same way if the reader's client renders reposts.
Posting the original event to multiple relays-as-groups is trivial, since all you have to do is send the event to the relay. Posting to multiple rooms simultaneously by appending multiple `h` tags is however not recommended, since group relays/clients are incentivised to protect themselves from spam by rejecting events with multiple `h` tags (similar to how events with multiple `t` tags are sometimes rejected).
## Privacy
Currently, it's recommended to include a [NIP 70](https://github.com/nostr-protocol/nips/blob/master/70.md) `-` tag on content posted to relays-as-groups to discourage replication of relay-specific content across the network.
Another slightly stronger approach would be for group relays to strip signatures in order to make events invalid (or at least deniable). For this approach to work, users would have to be able to signal that they trust relays to be honest. We could also [use ZkSNARKS](https://github.com/nostr-protocol/nips/pull/1682) to validate signatures in bulk.
In any case, group posts should not be considered "private" in the same way E2EE groups might be. Relays-as-groups should be considered a good fit for low-stakes groups with many members (since trust deteriorates quickly as more people get involved).
## Membership
There is currently no canonical member list published by relays (except for NIP 29 managed rooms). Instead, users keep track of their own relay and room memberships using `kind 10009` lists. Relay-level memberships are represented by an `r` tag containing the relay url, and room-level memberships are represented using a `group` tag.
Users can choose to advertise their membership in a RAG by using unencrypted tags, or they may keep their membership private by using encrypted tags. Advertised memberships are useful for helping people find groups based on their social graph:
![Discover]()
User memberships should not be trusted, since they can be published unilaterally by anyone, regardless of actual access. Possible improvements in this area would be the ability to provide proof of access:
- Relays could publish member lists (although this would sacrifice member privacy)
- Relays could support a new command that allows querying a particular member's access status
- Relays could provide a proof to the member that they could then choose to publish or not
## Moderation
There are two parts to moderation: reporting and taking action based on these reports.
Reporting is already covered by [NIP 56](https://github.com/nostr-protocol/nips/blob/master/56.md). Clients should be careful about encouraging users to post reports for illegal content under their own identity, since that can itself be illegal. Relays also should not serve reports to users, since that can be used to _find_ rather than address objectionable content.
Reports are only one mechanism for flagging objectionable content. Relay operators and administrators can use whatever heuristics they like to identify and address objectionable content. This might be via automated policies that auto-ban based on reports from high-reputation people, a client that implements [NIP 86](https://github.com/nostr-protocol/nips/blob/master/86.md) relay management API, or by some other admin interface.
There's currently no way for moderators of a given relay to be advertised, or for a moderator's client to know that the user is a moderator (so that they can enable UI elements for in-app moderation). This could be addressed via [NIP 11](https://github.com/nostr-protocol/nips/blob/master/11.md), [LIMITS](https://github.com/nostr-protocol/nips/pull/1434), or some other mechanism in the future.
## General best practices
In general, it's very important when developing a client to assume that the relay has _no_ special support for _any_ of the above features, instead treating all of this stuff as [progressive enhancement](https://developer.mozilla.org/en-US/docs/Glossary/Progressive_Enhancement).
For example, if a user enters an invite code, go ahead and send it to the relay using a `kind 28934` event. If it's rejected, you know that it didn't work. But if it's accepted, you don't know that it worked - you only know that the relay allowed the user to publish that event. This is helpful, becaues it may imply that the user does indeed have access to the relay. But additional probing may be needed, and reliance on error messages down the road when something else fails unexpectedly is indispensable.
This paradigm may drive some engineers nuts, because it's basically equivalent to coding your clients to reverse-engineer relay support for every feature you want to use. But this is true of nostr as a whole - anyone can put whatever weird stuff in an event and sign it. Clients have to be extremely compliant with Postell's law - doing their absolute best to accept whatever weird data or behavior shows up and handle failure in any situation. Sure, it's annoying, but it's the cost of permissionless development. What it gets us is a completely open-ended protocol, in which anything can be built, and in which every solution is tested by the market.