Desktop hardcodes HOSTED_COMMUNITY_LIMIT = 5 while the relay enforces the env-configurable max_communities_per_owner() — UI drifts in both directions once BUZZ_MAX_COMMUNITIES_PER_OWNER is set
- Dominant language
- Rust
- Stars
- 32.7k
- Forks
- 4.3k
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 253
Description
## Problem
The per-owner community limit is enforced by the relay from an **env-configurable** value but gated in the desktop UI by a **hardcoded constant**. The two agree today only because both were edited by hand in the same PR. The moment a deployment sets `BUZZ_MAX_COMMUNITIES_PER_OWNER`, the client and the server disagree and the UI is wrong in one of two directions.
**Server (authoritative, env-configurable):** `crates/buzz-db/src/relay_members.rs:414` `max_communities_per_owner()` → `effective_owner_limit(env BUZZ_MAX_COMMUNITIES_PER_OWNER)`, falling back to `MAX_COMMUNITIES_PER_OWNER = 5` (`relay_members.rs:406`) only when the env var is absent/unparseable/non-positive (`relay_members.rs:428-432`). Enforced in both write paths: create (`crates/buzz-db/src/lib.rs:1440`) and transfer (`relay_members.rs:528`).
**Client (hardcoded):** `desktop/src/features/communities/hostedCommunityApi.ts:4` `export const HOSTED_COMMUNITY_LIMIT = 5;`
Gating sites, all reading that constant:
| File:line | Behavior driven by the constant |
|---|---|
| `hostedCommunityApi.ts:72` | `limit_reached` error copy: "You've reached the limit of **5** hosted communities." |
| `HostedCommunityCreateFlow.tsx:192` | `atCommunityLimit` → disables the name input (`:411`) and the submit button (`:447`), and short-circuits `create()` (`:225`) |
| `HostedCommunityCreateFlow.tsx:375` | Feedback copy "the limit of **5**" |
| `HostedCommunityOnboarding.tsx:243` | `atCommunityLimit` → same disable/short-circuit pattern |
| `HostedCommunityOnboarding.tsx:356` | Feedback copy |
| `HostedCommunitiesSettingsCard.tsx:363` | Blocks `createCommunity()` |
| `HostedCommunitiesSettingsCard.tsx:415` | `atCommunityLimit` |
| `HostedCommunitiesSettingsCard.tsx:558` | Renders "**N of 5** used" |
| `HostedCommunitiesSettingsCard.tsx:622` | "You've reached the limit of **5** hosted communities." |
## Both failure directions
**Env set below 5** (e.g. `BUZZ_MAX_COMMUNITIES_PER_OWNER=3`): the client happily lets the user name and submit a 4th community. The relay rejects it, and the desktop renders the mapped `limit_reached` copy — which says *"the limit of 5"*. The user is told a number that is not the limit they just hit. The settings card also reads "3 of 5 used" while they are in fact maxed out.
**Env set above 5** (e.g. `7`): the client caps at 5 — input disabled, button disabled, "5 of 5 used" — while the relay would have accepted two more. The operator raised the limit and the UI silently ignores it, which is exactly the multi-tenant case #2600 asked for and #2599 shipped.
## Why this is not a one-line fix
**The number is nowhere on the wire.** The client cannot learn the effective limit today:
- `GET /operator/communities` (`crates/buzz-relay/src/api/operator.rs:331-339`) returns `owner_pubkey` + `communities[]` (`community_id`, `host`, `created_at`, `archived_at`) — no limit field.
- `ProvisionCommunityResponse` (`crates/buzz-relay/src/handlers/community_provisioning.rs:57-69`) has `community_id`, `host`, `status`, `owner_pubkey` — no limit field.
- The rejection string carries no number: `"limit_reached: owner already owns the maximum number of communities"` (`community_provisioning.rs:296`) and the transfer twin at `operator.rs:426`.
So an honest fix is an **API-shape change** plus a UI read: expose the effective limit on a response the client already calls (the list endpoint is the natural home — it is what populates `communities.length`), type it in `HostedCommunitiesResponse` (`hostedCommunityApi.ts:39-43`), and have the three gating sites read it with the constant demoted to a fallback for pre-upgrade servers. That is production code touching a public response shape, so it wants its own review — deliberately **not** folded into #4148, which is test-only and green.
## Defect family
This is the third instance of one shape: *a test or client hardcodes a value the code derives*.
1. `crates/buzz-db/src/lib.rs` owner-limit test seeded `0..3` while the code enforced the function — broke when #3829 raised the default. Fixed in #4148.
2. `crates/buzz-db/src/relay_members.rs` transfer-limit test, same. Fixed in #4148.
3. `crates/buzz-relay/src/api/operator.rs:1086` seeded from the **const** while the code enforces the **function** — survived #3829 because the const bump moved test and code together, but inverts as soon as the env is set. Found post-open, fixed in #4148 (`7db75a607`); Eva RED/GREEN-proved it at `BUZZ_MAX_COMMUNITIES_PER_OWNER=3`.
4. **This issue** — the same split, in production code rather than tests.
The generalizable rule: when a value becomes runtime-configurable, every consumer that duplicates it becomes a latent bug, and the const left behind as a default makes the duplication invisible to review. #2599 made the limit configurable; #3829 then edited the const in two files by hand — which is the tell.
## Repro (requires wiring the env)
1. Start the relay with `BUZZ_MAX_COMMUNITIES_PER_OWNER=3`.
2. As one owner, create 3 hosted communities from the desktop settings card.
3. The card shows "3 of 5 used" and leaves the create form enabled.
4. Submit a 4th with a fresh, available name → relay 409 `limit_reached`, and the desktop renders "You've reached the limit of 5 hosted communities."
Note step 1 is the reason this is latent: `BUZZ_MAX_COMMUNITIES_PER_OWNER` is set nowhere in this repo. I grepped the whole working tree (excluding `target/`) and it appears only in `relay_members.rs` — the reader itself plus its own unit tests. No chart, template, workflow, or compose file sets it. Same "latent until someone wires the env" posture as `BUZZ_REPLICA_READ_MAX_AGE_MS` in #4124.
## Caveat I could not close
The desktop does **not** call the relay's operator API directly. It calls Builderlab — `desktop/src-tauri/src/builderlab.rs:15` (`https://app.builderlab.xyz/api/goose`), via `POST /v1/buzz/communities/list` (`builderlab.rs:550`) and `POST /v1/buzz/communities` (`builderlab.rs:582`). That service is **not in this repo** and I did not read it. I verified the relay emits the `limit_reached:` prefix and the desktop maps a `limit_reached` code, but I cannot say whether the middle hop already forwards a limit value or normalizes the error. Anyone implementing the fix should check the Builderlab side first — if it already has the number, the change may be smaller than described; if it does not, it needs a change too.
## Non-findings, checked so they don't get re-litigated
- **Archived communities are counted consistently on both sides.** The server counts `relay_members WHERE role = 'owner'` regardless of `archived_at` (`lib.rs:1433-1438`, `relay_members.rs:521-527`), and `archive_community_owned_by` only sets `archived_at` without touching the owner row (`lib.rs:1486-1507`). The client uses `communities.length`, i.e. also includes archived. So the settings-card copy "Transfer one to free up a slot" is correct and archiving-vs-counting is not part of this bug.
- **Onboarding's `activeCommunities` filter is display-only.** `HostedCommunityOnboarding.tsx:236-238` filters archived for rendering, but `:243` gates on unfiltered `communities.length` — which matches the server. Not a defect.
## Provenance
Found while verifying #4148 (follow-up to #4124). All file:line references read at #4148 head `7db75a607` via `git show`, in `REPOS/buzz-pr4124-sami`. Related: #2600 (asked for configurability), #2599 (shipped `BUZZ_MAX_COMMUNITIES_PER_OWNER`), #3829 (raised both hardcoded values by hand), #4148 (fixed instances 1-3).
Contributor guide
Research direction
Start with the relay list response in crates/buzz-relay/src/api/operator.rs and the desktop types and gating sites in hostedCommunityApi.ts, HostedCommunityCreateFlow.tsx, HostedCommunityOnboarding.tsx, and HostedCommunitiesSettingsCard.tsx. Check the Builderlab endpoints in desktop/src-tauri/src/builderlab.rs first to determine whether the limit already crosses that boundary. Done means the effective configured limit reaches the desktop and all displayed, disabled, and submission-limit behavior agrees with the relay.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust, typescript
- Domain
- api, backend-api-design, frontend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100