Community highlights
A community can carry up to four marks — small pills rendered under its detail-page hero — each stating a real, verified fact about the community rather than a decorative label. This page is the durable reference; the design rationale, every dropped-field finding, and the copy contract behind each ruling live in the UI-SPEC.
These are not badges. "Badge" is a locked product noun for the Achievement Hall's earned, rarity-tiered user badges (see Achievements). A community mark describes a community, not a person — it has no rarity and does not unlock anything. No user-facing string on this surface uses the word "badge." Internal identifiers keep their existing names as identifiers: Types::CommunityBadgesType, Community#badges, useCommunityBadges, COMMUNITY_BADGES_QUERY.
The four marks
| Label (verbatim) | Constant(s) | Threshold, as a sentence |
|---|---|---|
| 1,000 members | CLUB_MEMBER_THRESHOLD = 1_000 | 1,000 or more people are currently members of this community. |
| 500 goals done | WINS_THRESHOLD = 500 | Members of this community have completed 500 or more goals, all-time. |
| 10 members reached 100 days | STREAK_DAYS = 100, STREAK_HOLDERS_THRESHOLD = 10 | 10 or more of this community's current members have each, at some point, reached a personal streak of 100 days or longer. |
| Staff pick | — (Community#is_featured) | An Objectuve admin has marked this community as featured. |
All four constants live in rails_api/app/models/community.rb:25-28. The computation is Community#badges (community.rb:144-151), and the field descriptions on Types::CommunityBadgesType (rails_api/app/graphql/types/community_badges_type.rb) restate each threshold verbatim — they are the best existing prose statement of each rule and this page's source of truth for the wording above.
streak100 — read this before touching the label
It is not "a 100-day streak." It is: ten or more of a community's current members have each, at some point, reached a longest streak of 100 days or more.
The computation joins on users.longest_streak, never current_streak:
streak100: members.joins(:user).where('users.longest_streak >= ?', STREAK_DAYS).count >= STREAK_HOLDERS_THRESHOLDcurrent_streak has no decay job — nothing writes it back down when a user goes inactive — so a member who stopped checking in months ago can still carry a stale current_streak value. Reading it would make the mark a lie by omission the moment activity lapsed. longest_streak is a monotonic personal record: once set, it only ever goes up. A mark built on it can under-count while data is stale, but it can never over-claim — the safe direction for a threshold. This is the same reasoning Streak Motifs applies to longest_streak elsewhere in the product.
That's also why the label says "reached," not "hold" or "are on" — ten people crossed the line at some point; nothing here claims they're all mid-streak today.
featured — an editorial flag, not an earned mark
featured reads Community#is_featured, set by an admin via Admin::SetCommunityFeatured on the /communities/curation screen. It is explicitly not earned — no member action produces it — and it explicitly does not affect discovery ranking: discovery ordering runs on editorial_slot / editorial_position (see the editorial discovery rails in Communities), which is_featured never touches. The GraphQL field description was rewritten to say exactly this; the prior description ("Community is currently featured on the discovery page") was false — no discovery surface has ever read the flag.
The breaking schema change
Types::CommunityBadgesType shipped ten fields. Six were removed — any client still selecting one of these now gets a query error, not a false:
| Removed field | Why |
|---|---|
verified | communities.is_verified has no production write path — see "The earlySupporter finding" below. |
topActive | 20% of the health_score it would have ranked on is a hardcoded response_score = 50 (community.rb:125) — fixing the activity proxy alone can't fix that. The live {n} posts today pill on the hero is a better, already-honest liveness signal. |
earlySupporter | No launch-date anchor exists to compute it from. The 30-day founding window (FOUNDING_WINDOW_DAYS) is already surfaced separately, as a transient "Founding" status, not a permanent mark. |
topContributor | "High ratio" is not a fixed threshold — a ratio-based mark toggles on and off week to week, and the concept describes a person, not a community. |
allyMagnet | The data can't distinguish "allies who met in this community" from "allies who both happen to be members of it." Every honest phrasing was unintelligible; every intelligible phrasing over-claimed. |
perfectMonth | No stable definition of "all active members" at scale, and the bar is near-unachievable once a community has more than a handful of members. |
The rule this milestone established
A community mark ships only when its threshold is computable from real data, and its label states that threshold. No placeholder
false.
A mark that can never legitimately flip to true — a hardcoded false wearing a real database column — is not a signal. Ship nothing rather than ship that.
The earlySupporter finding — a worked example, not just history
Before this milestone, Community#badges included fields whose backing data was never wired to production. communities.is_verified is the clearest case: the column exists, is queryable, and CommunityBadgesType exposed a field for it — but grepping every writer of is_verified turns up exactly one, rails_api/lib/tasks/social_simulator.rake — a dev/demo seeding rake task. There is no admin mutation, no interaction, no controller path that sets it in production. is_verified is false for every real community, permanently, and always will be until a production writer exists.
That is not a badge with a bug — it's a hardcoded false wearing a real column. The check a future contributor can run before adding a new mark: grep every writer of the field the mark reads. If the only writer is a seed script, a migration, or nothing at all, the mark is not ready to ship, no matter how real the column looks.
(CommunityType.isVerified — the field on the base community type, distinct from the removed CommunityBadgesType.verified — is unaffected by this change and stays.)
Nullable payload, not a hidden empty state
communityBadges(communityId:) is a nullable field (rails_api/app/graphql/resolvers/community_queries.rb). It returns null when Community.visible_to(current_user).find_by(public_id: community_id) finds nothing — the community doesn't exist, is soft-deleted, or falls outside the caller's demo/real visibility partition — not an error, and not all-false. (A room-access denial past that point, via CommunityAccessPolicy, takes a different path entirely: it raises a GraphQL::ExecutionError, not null.) The frontend (useCommunityBadges in ionic_frontend/src/composables/useCommunity.ts, consumed by views/Community.vue) treats "the query resolved to null" and "the query threw" as the same rendered state: an inert error pill reading "Highlights didn't load."
That collapses two genuinely different situations — the query failed and the query succeeded and told you nothing — into one visible state deliberately, because a caller who can't see the community shouldn't get a more specific signal than "didn't load." The one state that renders nothing is a successful response where all four marks are false; that is the only case with no error, no loading skeleton, and no <ul> in the DOM at all.
Where it renders
CommunityBadges.vue (ionic_frontend/src/components/community/CommunityBadges.vue — note community/, singular, not the communities/ discovery-surface directory) renders on ionic_frontend/src/views/Community.vue, directly below the hero and above the creator-progress callout. It's suppressed entirely in team rooms (v-if="!isTeamRoom") — a private, seat-based room has no use for "1,000 members" or "Staff pick."
Last updated: 2026-09-02 (v4.50). · Version: v4.7.0