Identity is permanently bound to a home community by its first-ever event, and only a profile event creates the binding
- Dominant language
- Rust
- Stars
- 32.7k
- Forks
- 4.3k
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 253
Description
## 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
1. Generate a new keypair.
2. 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.
3. Generate a second new keypair.
4. 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`.
5. Send from the second key to the same channel. It succeeds.
## What would help
Any one of these, in rough order of preference:
1. **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.
2. **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.
3. **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.
Contributor guide
Assessment
This issue has not been assessed yet.