block / block/buzz

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

Open
#6,498 0 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.