Skip to content

feat: Squads — social streak groups, Kudos, weekly leaderboard (Strava Clubs pattern) #220

Description

@Eliaaazzz

Summary

Build Squads — small (3–10 person) social groups with a shared streak, Kudos on logged meals, and a weekly leaderboard ranked by average Daily Score. This is the AuraFitness adaptation of Strava Clubs + Duolingo group streaks: the two retention mechanics with the strongest published evidence in consumer health.

Strava Club members are 3.5× more likely to remain active after 12 months. Apps with social streak features show avg streak length 5.69 days vs 4.25 days without. Source: Trophy / StriveCloud Strava case study.

This is week 1, feature 1 of 3 (the other two: Behavior Insights, Nutrition Challenges).


Why this fits AuraFitness

  • Differentiator: MyFitnessPal/Noom/MacroFactor have no native social squad layer for nutrition. We own this.
  • Friction budget: Kudos = 1 tap. Squad creation flow ≤3 screens.
  • Brand fit: Orange #F97316 works for energy/movement (Strava-adjacent), violet #A78BFA keeps dark-mode premium (Discord-adjacent — fitting for social).

User Stories

US-1.1 — As a new user, I want to create or join a Squad with a 6-character invite code so that I can log alongside friends without a public profile system.

US-1.2 — As a Squad member, I want to give Kudos (1 tap) to a teammate's logged meal so that my friends feel seen for showing up.

US-1.3 — As a Squad member, I want a shared Squad streak that survives if at least one member logs a meal each day so that nobody drops the team on a bad day (lowers loss-aversion penalty without killing the mechanic).

US-1.4 — As a Squad member, I want a weekly leaderboard ranked by 7-day avg Daily Score so I see where I stand without a permanent KOM-style ranking that punishes new joiners.

US-1.5 — As a Squad owner, I want to remove a member or leave the squad without losing my personal streak.


Acceptance Criteria

AC-1 Squad lifecycle

  • User can create a Squad with name (max 30 chars) and emoji avatar; system generates 6-char invite code (collision-checked).
  • User can join via invite code; max 10 members enforced server-side (HTTP 409 if full).
  • User can be in at most 3 Squads simultaneously (HTTP 409 on 4th).
  • Owner can remove members; last-member-leaving deletes the Squad.

AC-2 Kudos

  • Kudos is idempotent per (user, meal_log_id) pair — second tap removes it (toggle).
  • Kudos count visible in <100ms after tap (optimistic UI + reconcile).
  • Self-Kudos is rejected (HTTP 400).
  • Kudos only allowed on meal logs from same Squad members within last 7 days.

AC-3 Squad streak

  • Squad streak increments by 1 at midnight (squad timezone) if ≥1 member logged a meal that day.
  • Streak resets to 0 after a missed day; the day before reset is preserved as longest_streak.
  • Streak displays a 🔥 flame badge with 5 milestone tiers (3, 7, 14, 30, 100 days) — same tier system as personal StreakBadge.

AC-4 Leaderboard

  • Weekly leaderboard sorted by avg Daily Score (descending) over rolling 7 days.
  • Members with <3 logged days in the window are shown as "Warming up" and excluded from ranking (avoids cold-start penalty).
  • Leaderboard refreshes on app foreground + every 5 min while open.

AC-5 Non-functional

  • All Squad endpoints return P95 <250ms with Redis cache hit; <600ms cache miss.
  • Privacy: a user not in a Squad cannot read its members or activity (RLS / service-layer auth).
  • Wrapped with withErrorBoundary per project convention.

UI / UX Spec

Inspired by: Strava Clubs (social grouping), Duolingo group streak (shared loss aversion), Discord server avatars (emoji identity).

Screens

  1. SquadsListScreen — Bento card grid of user's squads. Each card: emoji avatar + name + current streak flame + "X members logged today". Empty state CTA: "Create" / "Join with code".
  2. SquadDetailScreen — header with squad streak gauge (reuses DailyScoreCard arc style), tab switcher: Activity | Leaderboard | Members.
  3. SquadCreateModal — name input, emoji picker (24 curated), submit → reveals invite code with copy button.
  4. JoinSquadModal — 6-digit segmented input (Apple-style code entry).

Components

  • SquadCard — glass morphism (backdrop-filter: blur(28px)), 16:9 aspect.
  • KudosButton — flame icon, spring animation (damping 12, stiffness 150) on tap, haptic medium.
  • SquadLeaderboardRow — rank pill + avatar + name + Daily Score arc (mini, 32px).
  • SquadStreakHeader — large flame + day count, animated number rollup on increment.

Theming

  • Primary CTA: BRAND_COLORS.primary (#F97316 light / #A78BFA dark).
  • Kudos active state: #F97316 fill; inactive: rgba(255,255,255,0.6).
  • No hardcoded hex anywhere — all via theme.ts.

Animations

  • Kudos tap: scale 0.9 → 1.15 → 1.0 spring, 380ms.
  • Streak increment: number rollup with react-native-reanimated withSpring.
  • Leaderboard re-rank: LayoutAnimation slide.

Database Schema (PostgreSQL + Flyway)

-- V20260507_001__create_squads.sql
CREATE TABLE squads (
  id              BIGSERIAL PRIMARY KEY,
  name            VARCHAR(30) NOT NULL,
  emoji           VARCHAR(8)  NOT NULL,
  invite_code     CHAR(6)     NOT NULL UNIQUE,
  owner_user_id   BIGINT      NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  current_streak  INT         NOT NULL DEFAULT 0,
  longest_streak  INT         NOT NULL DEFAULT 0,
  last_active_day DATE,
  timezone        VARCHAR(64) NOT NULL DEFAULT 'UTC',
  created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_squads_invite_code ON squads(invite_code);

CREATE TABLE squad_members (
  squad_id  BIGINT NOT NULL REFERENCES squads(id) ON DELETE CASCADE,
  user_id   BIGINT NOT NULL REFERENCES users(id)  ON DELETE CASCADE,
  role      VARCHAR(16) NOT NULL DEFAULT 'member', -- 'owner' | 'member'
  joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  PRIMARY KEY (squad_id, user_id)
);
CREATE INDEX idx_squad_members_user ON squad_members(user_id);

CREATE TABLE meal_log_kudos (
  meal_log_id BIGINT NOT NULL REFERENCES meal_logs(id) ON DELETE CASCADE,
  user_id     BIGINT NOT NULL REFERENCES users(id)     ON DELETE CASCADE,
  created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  PRIMARY KEY (meal_log_id, user_id)
);
CREATE INDEX idx_kudos_meal ON meal_log_kudos(meal_log_id);

Constraints:

  • CHECK ((SELECT COUNT(*) FROM squad_members WHERE squad_id = NEW.squad_id) <= 10) — enforced via trigger or service layer.
  • 3-squad-per-user cap enforced at service layer (cheaper than trigger).

API Contract (Spring Boot REST)

Method Path Body / Params Returns
POST /api/v1/squads {name, emoji} Squad
POST /api/v1/squads/join {inviteCode} Squad
GET /api/v1/squads Squad[] (user's squads)
GET /api/v1/squads/{id} SquadDetail
DELETE /api/v1/squads/{id}/members/{userId} 204
GET /api/v1/squads/{id}/leaderboard?window=7d LeaderboardEntry[]
POST /api/v1/meal-logs/{id}/kudos {kudosCount, kudoed: true}
DELETE /api/v1/meal-logs/{id}/kudos {kudosCount, kudoed: false}

Caching: /squads/{id} and /leaderboard cached in Redis with 60s TTL keyed by squad:{id}:leaderboard:{userId}. Invalidated on member change or new meal log.

Error format:

{ "error": { "code": "SQUAD_FULL", "message": "Squad has reached 10-member limit" } }

Tests

Unit (Spring Boot — JUnit 5 + Mockito)

  • SquadServiceTest
    • create_generatesUniqueInviteCode_retriesOnCollision
    • join_rejectsWhenSquadFull → throws SquadFullException
    • join_rejectsWhenUserInThreeSquads
    • removeMember_ownerCannotBeRemovedBySelf_unlessLastMember
  • KudosServiceTest
    • toggleKudos_idempotentOnDoubleCall
    • kudos_rejectedForSelf
    • kudos_rejectedForNonSquadMember
    • kudos_rejectedForMealOlderThan7Days
  • SquadStreakSchedulerTest
    • incrementsWhenAtLeastOneMemberLogged
    • resetsAfterMissedDay_preservesLongestStreak
    • respectsSquadTimezone

Unit (Frontend — Jest + React Native Testing Library)

  • KudosButton.test.tsx — toggles state, calls API once on rapid double-tap (debounce).
  • SquadStreakHeader.test.tsx — renders correct tier badge for [3, 7, 14, 30, 100] days.
  • SquadLeaderboardRow.test.tsx — "Warming up" label when <3 logged days.
  • useSquads.test.ts — hook caches, refetches on foreground.

E2E (Detox iOS + Playwright web)

  • e2e/squads.create-and-join.e2e.ts
    • User A creates squad → copies invite code → User B joins → both see each other in members list.
  • e2e/squads.kudos.e2e.ts
    • User A logs meal → User B taps Kudos → User A sees badge increment in real time (WebSocket).
  • e2e/squads.streak.e2e.ts
    • Mock clock advance 24h with 1 member logged → streak = 1. Advance another 24h with no logs → streak = 0, longest = 1.
  • e2e/squads.leaderboard.e2e.ts
    • 5 members log varying days → leaderboard ordering matches expected; <3-day member shown as "Warming up".

Day-by-day plan (5 working days)

Day Backend Frontend Tests
Mon Flyway migration + JPA entities; SquadRepository, SquadMemberRepository, KudosRepository Theme tokens, SquadCard component skeleton
Tue SquadService (create/join/leave) + invite code generator; controller + DTOs SquadsListScreen + SquadCreateModal + JoinSquadModal Service unit tests
Wed Kudos endpoint + Redis cache + WebSocket push on kudos event KudosButton, integrate into existing MealLogCard Kudos unit tests + RN component tests
Thu Squad streak scheduler (Quartz / @Scheduled) + leaderboard query (CTE over daily_scores) SquadDetailScreen with 3 tabs + leaderboard refresh Scheduler unit + leaderboard test
Fri Bug bash, P95 latency check, cache key audit Animation polish (Reanimated), AI disclaimer pass on any AI surface, error boundary wrap All Detox + Playwright e2e flows

Definition of Done

  • All ACs check.
  • Unit + e2e suites green in CI.
  • P95 endpoint latency <250ms (cache hit).
  • No hardcoded hex; theme audit clean.
  • withErrorBoundary on every new screen.
  • Feature flagged behind feature.squads.enabled for staged rollout.
  • Analytics events: squad_created, squad_joined, kudos_given, squad_streak_milestone.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions