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.guildMemberIdGuildMember.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)