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
AC-2 Kudos
AC-3 Squad streak
AC-4 Leaderboard
AC-5 Non-functional
UI / UX Spec
Inspired by: Strava Clubs (social grouping), Duolingo group streak (shared loss aversion), Discord server avatars (emoji identity).
Screens
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".
SquadDetailScreen — header with squad streak gauge (reuses DailyScoreCard arc style), tab switcher: Activity | Leaderboard | Members.
SquadCreateModal — name input, emoji picker (24 curated), submit → reveals invite code with copy button.
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
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.
This is week 1, feature 1 of 3 (the other two: Behavior Insights, Nutrition Challenges).
Why this fits AuraFitness
#F97316works for energy/movement (Strava-adjacent), violet#A78BFAkeeps 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
AC-2 Kudos
AC-3 Squad streak
longest_streak.StreakBadge.AC-4 Leaderboard
AC-5 Non-functional
withErrorBoundaryper project convention.UI / UX Spec
Inspired by: Strava Clubs (social grouping), Duolingo group streak (shared loss aversion), Discord server avatars (emoji identity).
Screens
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".SquadDetailScreen— header with squad streak gauge (reusesDailyScoreCardarc style), tab switcher:Activity | Leaderboard | Members.SquadCreateModal— name input, emoji picker (24 curated), submit → reveals invite code with copy button.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
BRAND_COLORS.primary(#F97316light /#A78BFAdark).#F97316fill; inactive:rgba(255,255,255,0.6).theme.ts.Animations
0.9 → 1.15 → 1.0spring, 380ms.react-native-reanimatedwithSpring.LayoutAnimationslide.Database Schema (PostgreSQL + Flyway)
Constraints:
CHECK ((SELECT COUNT(*) FROM squad_members WHERE squad_id = NEW.squad_id) <= 10)— enforced via trigger or service layer.API Contract (Spring Boot REST)
POST/api/v1/squads{name, emoji}SquadPOST/api/v1/squads/join{inviteCode}SquadGET/api/v1/squadsSquad[](user's squads)GET/api/v1/squads/{id}SquadDetailDELETE/api/v1/squads/{id}/members/{userId}204GET/api/v1/squads/{id}/leaderboard?window=7dLeaderboardEntry[]POST/api/v1/meal-logs/{id}/kudos{kudosCount, kudoed: true}DELETE/api/v1/meal-logs/{id}/kudos{kudosCount, kudoed: false}Caching:
/squads/{id}and/leaderboardcached in Redis with 60s TTL keyed bysquad:{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)
SquadServiceTestcreate_generatesUniqueInviteCode_retriesOnCollisionjoin_rejectsWhenSquadFull→ throwsSquadFullExceptionjoin_rejectsWhenUserInThreeSquadsremoveMember_ownerCannotBeRemovedBySelf_unlessLastMemberKudosServiceTesttoggleKudos_idempotentOnDoubleCallkudos_rejectedForSelfkudos_rejectedForNonSquadMemberkudos_rejectedForMealOlderThan7DaysSquadStreakSchedulerTestincrementsWhenAtLeastOneMemberLoggedresetsAfterMissedDay_preservesLongestStreakrespectsSquadTimezoneUnit (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.tse2e/squads.kudos.e2e.tse2e/squads.streak.e2e.tse2e/squads.leaderboard.e2e.tsDay-by-day plan (5 working days)
SquadRepository,SquadMemberRepository,KudosRepositorySquadCardcomponent skeletonSquadService(create/join/leave) + invite code generator; controller + DTOsSquadsListScreen+SquadCreateModal+JoinSquadModalKudosButton, integrate into existingMealLogCard@Scheduled) + leaderboard query (CTE overdaily_scores)SquadDetailScreenwith 3 tabs + leaderboard refreshDefinition of Done
withErrorBoundaryon every new screen.feature.squads.enabledfor staged rollout.squad_created,squad_joined,kudos_given,squad_streak_milestone.