Skip to content

Identity is permanently bound to a home community by its first-ever event, and only a profile event creates the binding #6498

Description

@WifiDan

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions