Summary
On a self-hosted relay, a pubkey appears to be bound permanently to a single "home" community by its first-ever signed event, and only some event types create that binding. Once bound, the identity can never post in another community, regardless of what membership rows exist there.
This contradicts docs/multi-tenant-conformance.md, which describes identity as portable and scoped per community. It is also invisible in every table an operator would think to check, so it presents as a membership bug and sends you to the wrong subsystem.
What the docs say
docs/multi-tenant-conformance.md:
Same pubkey can have one profile per community; users repost kind:0 in each community they join.
users gains community_id; keys/uniques are (community_id, pubkey), (community_id, lower(nip05_handle)), and (community_id, okta_user_id) where applicable.
A portable key may join multiple communities, but memberships, DMs, profiles, jobs, and presence do not bleed across them.
Read together, that describes identity as per-community and portable. Nothing in docs/, docs/spec/ or docs/formal/ describes a first-event binding or a home community.
What we observe
Reproduced repeatedly on our own relay while migrating 15 agent identities between communities:
A new keypair whose first-ever signed event is a message send:
- fails with
relay error 400: restricted: not a channel member
- creates no
users row at all, in any community
The same, but whose first-ever signed event is buzz users set-profile against the target relay URL:
- registers correctly —
SELECT community_id FROM users WHERE encode(pubkey,'hex')='...' immediately shows the target community
- messages then send and land normally
After binding, the identity cannot post in a different community even when:
channel_members has a correct row for it in the target community
relay_members has a correct row for it in the target community
- it connects with the correct
--relay-url / BUZZ_RELAY_URL for that community
Every send is still rejected with restricted: not a channel member.
Why this is worth fixing rather than documenting
The failure mode is maximally misleading. The error names channel membership; channel membership is demonstrably correct; so an operator concludes the membership API is broken. I believe #5512's Gap 3 is this same bug seen from that angle — that reporter's workaround is "publishing a kind 0 profile and reading it back to detect actual membership," which is not detecting anything, it is creating the binding.
Cost to us: a 15-agent migration that appeared to succeed, then failed, and had to be rolled back and redone once we understood the ordering. We only found it by running strace -f -e trace=network on a failing send.
Reproduction
- Generate a new keypair.
- As its first-ever action,
buzz messages send to a channel it has a valid channel_members row in. Observe 400 restricted: not a channel member, and no users row created.
- Generate a second new keypair.
- As its first-ever action,
buzz users set-profile --name ... with BUZZ_RELAY_URL set to the same community. Observe a users row with the correct community_id.
- Send from the second key to the same channel. It succeeds.
What would help
Any one of these, in rough order of preference:
- Make registration independent of event type — if an authenticated identity is admitted to a community, any first event should register it, not only a profile publish.
- Fail with an accurate error.
restricted: not a channel member is wrong when the row exists; something like identity not registered in this community would have saved us a day.
- Document it, if the binding is intended. In that case
docs/multi-tenant-conformance.md's portability language needs a caveat, and the required ordering should be stated explicitly.
Environment
Self-hosted buzz-relay (ghcr.io/block/buzz:main, image built 2026-08-09), Postgres 17, 8 communities on one relay, ~26 headless agent identities provisioned via CLI rather than Desktop. Happy to provide table dumps or re-run the reproduction with more instrumentation if useful.
Summary
On a self-hosted relay, a pubkey appears to be bound permanently to a single "home" community by its first-ever signed event, and only some event types create that binding. Once bound, the identity can never post in another community, regardless of what membership rows exist there.
This contradicts
docs/multi-tenant-conformance.md, which describes identity as portable and scoped per community. It is also invisible in every table an operator would think to check, so it presents as a membership bug and sends you to the wrong subsystem.What the docs say
docs/multi-tenant-conformance.md:Read together, that describes identity as per-community and portable. Nothing in
docs/,docs/spec/ordocs/formal/describes a first-event binding or a home community.What we observe
Reproduced repeatedly on our own relay while migrating 15 agent identities between communities:
A new keypair whose first-ever signed event is a message send:
relay error 400: restricted: not a channel memberusersrow at all, in any communityThe same, but whose first-ever signed event is
buzz users set-profileagainst the target relay URL:SELECT community_id FROM users WHERE encode(pubkey,'hex')='...'immediately shows the target communityAfter binding, the identity cannot post in a different community even when:
channel_membershas a correct row for it in the target communityrelay_membershas a correct row for it in the target community--relay-url/BUZZ_RELAY_URLfor that communityEvery send is still rejected with
restricted: not a channel member.Why this is worth fixing rather than documenting
The failure mode is maximally misleading. The error names channel membership; channel membership is demonstrably correct; so an operator concludes the membership API is broken. I believe #5512's Gap 3 is this same bug seen from that angle — that reporter's workaround is "publishing a kind 0 profile and reading it back to detect actual membership," which is not detecting anything, it is creating the binding.
Cost to us: a 15-agent migration that appeared to succeed, then failed, and had to be rolled back and redone once we understood the ordering. We only found it by running
strace -f -e trace=networkon a failing send.Reproduction
buzz messages sendto a channel it has a validchannel_membersrow in. Observe400 restricted: not a channel member, and nousersrow created.buzz users set-profile --name ...withBUZZ_RELAY_URLset to the same community. Observe ausersrow with the correctcommunity_id.What would help
Any one of these, in rough order of preference:
restricted: not a channel memberis wrong when the row exists; something likeidentity not registered in this communitywould have saved us a day.docs/multi-tenant-conformance.md's portability language needs a caveat, and the required ordering should be stated explicitly.Environment
Self-hosted
buzz-relay(ghcr.io/block/buzz:main, image built 2026-08-09), Postgres 17, 8 communities on one relay, ~26 headless agent identities provisioned via CLI rather than Desktop. Happy to provide table dumps or re-run the reproduction with more instrumentation if useful.