Guild Roster System
Overview
The guild roster system uses a hybrid approach that balances complete roster visibility for officers with user privacy and data freshness.
Architecture
Two-Tier System
1. GuildMember (Lightweight Roster Entries)
- Minimal public WoW data (character name, realm, rank)
- Created from Battle.net roster API
- Exists whether user has signed up or not
- Tracks claim status and roster history
2. Character (Full User-Owned Records)
- Complete character data from Battle.net
- Only created when user syncs via OAuth
- Includes avatar, class, race, level, etc.
- Auto-updates when user re-syncs
Database Schema
model GuildMember {
id String @id
guildId String
characterName String
realm String
wowRank Int
// Claim status
claimedByCharacter Character? // One-to-one
claimedAt DateTime?
// Roster tracking
lastSeenInRoster DateTime
leftGuildAt DateTime? // Tracks when they left guild
}
model Character {
// ... full character data
guildMemberId String? @unique
guildMember GuildMember? // Link to claimed roster entry
}
How It Works
Daily Roster Sync (Automated)
Scheduled every day at 3 AM.
-
For each active guild (has at least one claimed member):
- Fetch roster from Battle.net API
- Upsert
GuildMemberrecords (create or update) - Update ranks if changed in WoW
- Mark characters who left as
leftGuildAt: <date>
-
Uses access token from any claimed guild member
-
Tracks
Guild.lastRosterSynctimestamp
User Character Sync (On-Demand)
Triggered when a user clicks "Sync Characters".
- User authenticates via Battle.net OAuth
- Fetch all characters from Battle.net
- Create/update
Characterrecords - Auto-claim matching
GuildMemberrecords:- Match by character name + realm
- Link
Character.guildMemberId→GuildMember.id - Set
GuildMember.claimedAttimestamp
Example Flow
Day 1:
- Officer clicks "Sync Characters"
- Creates full Character records for their 3 characters
- Auto-claims 3 matching GuildMember entries
- Daily sync runs at 3 AM → Creates GuildMember for all 100 guild members
Day 2:
- New member "Zogthrak" joins guild
- Daily sync at 3 AM → Creates unclaimed GuildMember for Zogthrak
- Zogthrak signs up and syncs → Auto-claims their GuildMember entry
Day 5:
- Member "Oldplayer" leaves guild
- Daily sync at 3 AM → Sets Oldplayer's leftGuildAt
- Guild roster now shows 99 active + 1 inactive
UI Features Enabled
For Guild Leaders/Officers
Complete Roster View:
✅ Claimed Members (42)
- Full character data
- Auto-updated via user syncs
- Avatar, class, level visible
⚪ Unclaimed Members (38)
- Name and rank visible
- "Invite to claim" button
- Shows as "Not synced"
🚫 Inactive Members (8)
- Marked with "Left guild on..."
- Historical data preserved
Attendance Tracking:
- Create event → Invite all roster members
- Track attendance for claimed & unclaimed
- Send "claim your account" reminders
Recruitment Metrics:
- "15 members haven't claimed accounts"
- "Send invite emails" button
- Track signup conversion
For Regular Members
Automatic Claiming:
- Sign up → Sync characters → Auto-claimed
- No manual "claim" flow needed
- Seamless user experience
Privacy:
- Only public WoW data stored until claimed
- Full data requires OAuth consent
- User controls their character sync
Benefits
✅ Complete Rosters Immediately
Officers see full guild roster without waiting for everyone to sign up
✅ No Privacy Concerns
Only public WoW data (name, rank) until user claims via OAuth
✅ Fresh Data
- Claimed characters: Updated on user sync (on-demand)
- Roster structure: Updated daily (automated)
- Best of both worlds
✅ Scalable
- Battle.net API called once per guild per day
- Respects rate limits
- Minimal database load
✅ Attendance Ready
Track attendance for all members, not just those who signed up
✅ Recruitment Tool
"30% of guild hasn't claimed accounts" → Send invites
API Endpoints
Organization Detail (with Roster Pagination)
GET /api/v1/organizations/:id?page=1&pageSize=50&sortBy=rank&sortOrder=asc
The main organization detail endpoint includes paginated roster members for guild-type organizations.
Query Parameters:
page(number) - Page number (default: 1)pageSize(number) - Items per page (default: 50)sortBy-rank|name|lastSeen|status(default: rank)sortOrder-asc|desc(default: asc)
Response (roster fields):
{
"id": "uuid",
"name": "Guild Name",
"realm": "Area 52",
"faction": "HORDE",
"rosterMembers": [
{
"id": "uuid",
"characterName": "Thrall",
"realm": "area-52",
"wowRank": 0,
"avatarUrl": "https://...",
"characterClass": "shaman",
"claimedAt": "2026-01-15T...",
"lastSeenInRoster": "2026-02-16T...",
"lastLoginAt": "2026-02-15T...",
"claimedByCharacter": {
"id": "uuid",
"name": "Thrall",
"level": 80,
"characterClass": "shaman",
"avatarUrl": "https://...",
"user": { "id": "uuid", "displayName": "PlayerName" },
"memberships": [
{
"id": "uuid",
"role": { "id": "uuid", "name": "Guild Master", "wowRank": 0 }
}
]
}
},
{
"id": "uuid",
"characterName": "Zogthrak",
"realm": "area-52",
"wowRank": 5,
"avatarUrl": null,
"claimedAt": null,
"claimedByCharacter": null,
"lastSeenInRoster": "2026-02-16T..."
}
],
"rosterPagination": {
"page": 1,
"pageSize": 50,
"total": 120,
"totalPages": 3
},
"memberships": [],
"userPermissions": {}
}
Standalone Roster Endpoint (Legacy)
GET /api/v1/organizations/:id/roster
Separate roster endpoint via WowGuildAdapter. Simpler response without claimed character relations.
Configuration
Sync Schedule
Modify in /apps/api/src/jobs/roster-sync.service.ts:
@Cron(CronExpression.EVERY_DAY_AT_3AM) // Default: 3 AM daily
async syncAllGuildRosters() { ... }
Other options:
CronExpression.EVERY_12_HOURSCronExpression.EVERY_WEEK- Custom:
'0 2 * * *'(2 AM daily)
Manual Trigger
For testing or officer-initiated syncs:
// In a controller
await this.rosterSyncService.syncGuildRoster(
guildId,
guildName,
guildRealm,
accessToken
);
Real-Time Roster Sync Progress
WebSocket Integration
Manual roster syncs (both guild-level and admin-level) provide real-time progress updates via WebSocket.
Guild-Level Sync
Endpoint: POST /api/v1/organizations/:id/roster/sync
Response:
{
"sessionId": "uuid",
"message": "Roster sync started. Connect to WebSocket for progress updates."
}
Admin-Level Sync (All Guilds)
Endpoint: POST /api/v1/admin/sync-rosters
Response:
{
"message": "Roster sync started for 5 guild(s)",
"sessionIds": ["uuid1", "uuid2", "uuid3", "uuid4", "uuid5"]
}
Progress Tracking
Roster sync operates in two phases with combined progress tracking:
Phase 1: Member Processing
- Fetch roster from Battle.net API
- Upsert
RosterMemberrecords (create or update) - Track new/left members
- Auto-claim unclaimed members
- Sync roles and permissions
- Emits progress with
currentMemberfor each character
Phase 2: Avatar Enrichment
- Queue parallel avatar fetch jobs
- Fetch avatar URL and profile data (class, spec, hero spec, last login)
- Emits progress with
currentMemberfor each character - Session completes only after all avatar jobs finish
Background Processing:
- API returns
sessionIdimmediately after validating Battle.net token - Member processing and avatar enrichment happen asynchronously
- 1.5s delay before emitting first progress (allows frontend to subscribe)
WebSocket Events
Namespace: /roster-sync
Event: sync-progress - Updates during sync
{
"sessionId": "uuid",
"status": "processing",
"totalMembers": 120,
"processedMembers": 85,
"newMembers": 3,
"leftMembers": 2,
"avatarFetchTotal": 120,
"avatarFetchProcessed": 45,
"avatarFetchFailed": 1,
"progressPercentage": 65,
"startedAt": "2026-02-17T...",
"completedAt": null,
"currentMember": {
"characterName": "Thrall",
"realm": "area-52",
"status": "processing"
}
}
Event: sync-complete - All work finished (roster + avatars)
{
"sessionId": "uuid",
"status": "completed",
"totalMembers": 120,
"processedMembers": 120,
"newMembers": 3,
"leftMembers": 2,
"avatarFetchTotal": 120,
"avatarFetchProcessed": 119,
"avatarFetchFailed": 1,
"progressPercentage": 100,
"startedAt": "2026-02-17T...",
"completedAt": "2026-02-17T..."
}
Event: sync-error - Sync failed
{
"sessionId": "uuid",
"error": "Failed to fetch roster: 401 Unauthorized"
}
Frontend Integration
Guild Settings Page
React Hook: useRosterSync
const { progress, isSyncing, joinSession } = useRosterSync();
// Start sync (via RTK mutation)
const result = await syncRoster(guildId).unwrap();
joinSession(result.sessionId);
// Auto-connect to active sessions on mount
// Automatically detects and joins in-progress syncs
Progress Component: RosterSyncProgress
{
progress && <RosterSyncProgress progress={progress} />;
}
Shows:
- Combined progress bar (roster + avatar phases)
- Current member being processed with animated icon
- New/left member counts
- Real-time avatar fetch progress
- Auto-hides 5 seconds after full completion
Sync Button:
- Disabled during active sync
- Shows "Syncing..." text when
isSyncing - Re-enabled after session completes
Admin Dashboard
React Hook: useAdminRosterSync
const { state: rosterSyncState, isSyncing: rosterSyncing } =
useAdminRosterSync(sessionIds);
// Start batch sync
const res = await syncRosters().unwrap();
setRosterSessionIds(res.sessionIds);
Progress Component: AdminRosterSyncProgress
{
rosterSyncing && rosterSyncState && (
<AdminRosterSyncProgress state={rosterSyncState} />
);
}
Shows:
- Overall guild progress (X / N guilds)
- Current guild's member/avatar progress
- Per-guild progress bar
- Current character being processed
- Completion summary with failure count
- Auto-hides 10 seconds after all guilds complete
Multi-Session Handling:
- Subscribes to all session IDs from batch sync response
- Aggregates progress across all guilds
- Shows first active guild as "current"
- Tracks completed/failed counts
Sync Session Schema
model RosterSyncSession {
id String @id @default(uuid())
wowGuildId String // organizationId
userId String
status String // pending, processing, completed, failed
totalMembers Int
processedMembers Int
newMembers Int
leftMembers Int
avatarFetchTotal Int? // Phase 2: total avatar jobs queued
avatarFetchProcessed Int? // Phase 2: completed avatar jobs
avatarFetchFailed Int? // Phase 2: failed avatar jobs
startedAt DateTime
completedAt DateTime?
error String?
}
Active Session Detection
Endpoint: GET /api/v1/organizations/:id/roster/sync/active
Returns active session if status is pending or processing:
{
"active": true,
"sessionId": "uuid",
"status": "processing",
"totalMembers": 120,
"processedMembers": 85,
"avatarFetchTotal": 120,
"avatarFetchProcessed": 45
}
Used by frontend to auto-connect to in-progress syncs on page mount.
Polling Strategy
Admin Dashboard:
- Sync status: 30s polling (historical summary)
- Job queue counts: 30s polling (informational)
- WebSocket provides all real-time detail
- Polling only for static summary tables and aggregate counts
Sync Source Restrictions
Member management behaviour differs based on a guild's syncSource:
Blizzard-Synced Guilds (syncSource: blizzard)
Roles and membership are managed in-game. The app is read-only for these operations:
| Action | Allowed? | Reason |
|---|---|---|
| View roster | Yes | Officers see full roster from Battle.net sync |
| View role history | Yes | Read-only audit trail |
| Change member role | No | Roles come from in-game ranks |
| Remove member | No | Membership is controlled in-game |
| Create custom role | No | Role list is defined by WoW ranks 0–9 |
| Edit role permissions | Yes | Officers can customise what each rank can do in-app |
The Members tab shows an info banner explaining this, and the Create Custom Role button is hidden on the Ranks tab.
Backend guards reject these operations with a 400 Bad Request if attempted via API.
Standalone Guilds (syncSource: standalone)
All management actions are available — roles, membership, and custom roles are fully controlled within the app.
Future Enhancements
Phase 2: Invite System
- "Send claim invite" button for unclaimed members
- Email/Discord notifications
- Track invite acceptance rate
Phase 3: Historical Roster
- Track rank changes over time
- "Member promoted from rank 5 to rank 2"
- Tenure tracking: "Joined guild 3 years ago"
Phase 4: Smart Sync
- Only sync guilds with recent activity
- Pause syncing for inactive guilds
- Resume when officer views roster
Troubleshooting
Q: Roster not updating?
- Check
Guild.lastRosterSynctimestamp - Verify at least one claimed member with valid token
- Check API logs for errors at 3 AM
Q: Character not auto-claiming?
- Verify character name + realm match exactly
- Check if
GuildMemberalready claimed by another Character - Ensure character is in guild roster
Q: Seeing duplicate characters?
- One
Charactercan link to multipleGuildMember(multi-guild) - Each guild has separate
GuildMemberentry - This is expected behavior
Q: API rate limiting?
- Daily sync = 1 API call per guild
- 100 guilds = 100 API calls/day
- Well within Battle.net limits (36,000/hour)