Skip to main content

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.

  1. For each active guild (has at least one claimed member):

    • Fetch roster from Battle.net API
    • Upsert GuildMember records (create or update)
    • Update ranks if changed in WoW
    • Mark characters who left as leftGuildAt: <date>
  2. Uses access token from any claimed guild member

  3. Tracks Guild.lastRosterSync timestamp

User Character Sync (On-Demand)​

Triggered when a user clicks "Sync Characters".

  1. User authenticates via Battle.net OAuth
  2. Fetch all characters from Battle.net
  3. Create/update Character records
  4. Auto-claim matching GuildMember records:
    • Match by character name + realm
    • Link Character.guildMemberId → GuildMember.id
    • Set GuildMember.claimedAt timestamp

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_HOURS
  • CronExpression.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 RosterMember records (create or update)
  • Track new/left members
  • Auto-claim unclaimed members
  • Sync roles and permissions
  • Emits progress with currentMember for 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 currentMember for each character
  • Session completes only after all avatar jobs finish

Background Processing:

  • API returns sessionId immediately 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:

ActionAllowed?Reason
View rosterYesOfficers see full roster from Battle.net sync
View role historyYesRead-only audit trail
Change member roleNoRoles come from in-game ranks
Remove memberNoMembership is controlled in-game
Create custom roleNoRole list is defined by WoW ranks 0–9
Edit role permissionsYesOfficers 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.lastRosterSync timestamp
  • 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 GuildMember already claimed by another Character
  • Ensure character is in guild roster

Q: Seeing duplicate characters?

  • One Character can link to multiple GuildMember (multi-guild)
  • Each guild has separate GuildMember entry
  • 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)