# league-system
> Use when working with leagues, league rules, members, invites, or role permissions
- Author: Mason Campbell
- Repository: zekkathura/10K_Project
- Version: 20260124180122
- Stars: 0
- Forks: 0
- Last Updated: 2026-02-07
- Source: https://github.com/zekkathura/10K_Project
- Web: https://mule.run/skillshub/@@zekkathura/10K_Project~league-system:20260124180122
---
---
name: league-system
description: Use when working with leagues, league rules, members, invites, or role permissions
---
# League System
The League feature replaces House Rules with guild-like functionality. Users create or join leagues to manage custom game rules.
## Core Concepts
### Membership
- **Exclusive**: Users can only be in ONE league at a time
- **Invite-only**: No public join codes - moderators/owners invite players they've played with
- **Registered users only**: No guest support for leagues
### Permission Roles (3-tier)
| Role | View Rules | Add/Revoke Rules | Invite | Change Roles | Kick | Transfer Ownership | Edit League |
|------|------------|------------------|--------|--------------|------|-------------------|-------------|
| **Owner** | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| **Moderator** | ✓ | ✓ | ✓ | ✓ (not owner) | ✗ | ✗ | ✗ |
| **Member** | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
### Rule Lifecycle (Honor System)
Rules are managed in the League screen. No in-app voting - voting happens in person.
**Adding a Rule:**
1. Moderator/Owner taps "Add Rule"
2. Enters rule text and confirms honor-system questions:
- "Were 2+ league members present?" (Yes/No)
- "Did majority vote in favor?" (Yes/No)
3. Rule created immediately with `status='active'`
**Revoking a Rule:**
1. Moderator/Owner taps rule → "Revoke"
2. Same honor-system confirmations
3. Rule status changes to `'revoked'`
## Key Files
### Database
- `src/lib/leagueDatabase.ts` - All league database operations
- `src/lib/types.ts` - League, LeagueMember, LeagueRule, LeagueInvite, LeagueActivity types
### Screens & Modals
- `src/screens/LeagueScreen.tsx` - Main league screen (tabs: Rules, Members, Activity, Settings)
- `src/screens/CreateLeagueModal.tsx` - Create new league
- `src/screens/InvitePlayerModal.tsx` - Invite co-players to league
- `src/screens/InvitePendingModal.tsx` - Accept/decline incoming invites
- `src/screens/AddRuleModal.tsx` - Add new rule with honor-system confirmations
- `src/screens/RevokeRuleModal.tsx` - Revoke existing rule
- `src/screens/ChangeRoleModal.tsx` - Change member roles (with kick/transfer)
- `src/screens/EditLeagueModal.tsx` - Edit league name/description
- `src/screens/TransferOwnershipModal.tsx` - Transfer ownership to another member
### Migrations (run in order)
```
database/migrations/006_league_system.sql # Tables
database/migrations/007_league_rls.sql # Functions + RLS policies
database/migrations/007b_league_grants.sql # Table permissions
database/migrations/008_league_game_association.sql # Game linking
database/migrations/009_league_realtime.sql # Realtime subscriptions
database/migrations/010_migrate_extra_rules_to_league.sql # Data migration
```
## Database Tables
| Table | Purpose |
|-------|---------|
| `leagues` | Core league info (name, description, status) |
| `league_members` | User membership (one league per user via UNIQUE constraint) |
| `league_rules` | League-specific rules (replaces extra_rules) |
| `league_invites` | Pending invitations |
| `league_activity_log` | Audit trail for league events |
## Common Operations
### Using leagueDatabase.ts
```typescript
import {
getUserLeagueMembership,
getPendingInvitesForUser,
createLeague,
invitePlayer,
respondToInvite,
leaveLeague,
addRule,
revokeRule,
changeMemberRole,
transferOwnership,
kickMember,
getLeagueMembers,
getLeagueRules,
getLeagueActivityLog,
} from '../lib/leagueDatabase';
// Get user's league info
const { data, error } = await getUserLeagueMembership(userId);
// Create a league (user becomes owner)
const result = await createLeague(userId, 'League Name', 'Description');
// Add a rule
const result = await addRule(
leagueId,
'Rule text here',
'Proposer Name',
'Approver1, Approver2',
true, // hadMinMembers
true, // hadMajorityVote
gameId // optional - link to game where proposed
);
// Change member role
const result = await changeMemberRole(
leagueId,
targetUserId,
'moderator', // 'owner' | 'moderator' | 'member'
actorName,
targetName
);
```
## UI Patterns
### LeagueScreen Tabs
- **Rules**: List of active rules, search, sort by # or date, show revoked toggle
- **Members**: Member list with role badges, invite button (moderators+)
- **Activity**: Event log (joins, leaves, rule changes, invites)
- **Settings**: Edit league, transfer ownership, leave/delete league
### Role-Based UI Visibility
```typescript
const canModify = userRole === 'owner' || userRole === 'moderator';
const isOwner = userRole === 'owner';
// Show add rule button only for moderators+
{canModify && }
// Show kick button only for owner
{isOwner && }
```
### Confirmation Pattern
Destructive actions (kick, transfer, role change) use two-step confirmation:
```typescript
const [confirmAction, setConfirmAction] = useState(false);
const handleAction = async () => {
if (!confirmAction) {
setConfirmAction(true); // First tap shows confirmation
return;
}
// Second tap executes
await performAction();
};
```
## Edge Cases
| Scenario | Handling |
|----------|----------|
| User not in league | Show welcome screen + pending invites |
| Invite player in other league | Show disabled in list with league name |
| Owner tries to leave | Must transfer ownership first |
| Moderator changes owner role | Blocked - only owner can transfer |
| Delete league | Only owner, removes all members/rules |
## Realtime Subscriptions
League data uses Supabase realtime for live updates:
```typescript
// In LeagueScreen.tsx
useEffect(() => {
const channel = supabase
.channel(`league-${leagueId}`)
.on('postgres_changes', {
event: '*',
schema: 'public',
table: 'league_rules',
filter: `league_id=eq.${leagueId}`
}, handleRulesChange)
.subscribe();
return () => { supabase.removeChannel(channel); };
}, [leagueId]);
```