Skip to content

Flashback Challenges Architecture

  • Challenge list page: src/app/challenges/page.jsx
  • Challenge detail page: src/app/challenges/[challengeId]/page.jsx
  • FACEIT connect button: src/components/auth/FaceitConnectButton.tsx
  • Rules modal: src/components/modals/ChallengeRulesModal.tsx
  • Sidebar link: src/components/navigation/ChallengeSidebarSection.jsx

Current hooks live in src/hooks/useApi.ts:

  • useChallenges
  • useChallenge
  • useChallengeRegistration
  • useRegisterChallenge
  • useUnregisterChallenge
  • useChallengeScoreboard
  • useChallengeAnalytics
  • useMyChallengeAnalyticsSummaries
  • useChallengeParticipants
  • useChallengeParticipantCount
  • useUserChallengeRegistrations

Deprecated but still present:

  • src/hooks/useChallenges.ts
  • src/lib/api/challenges.client.ts

When touching client/shared code, prefer the existing API helpers and OpenAPI client:

  • apiClient from src/lib/api/client.ts
  • apiGet, apiPost, apiPut, apiDelete from src/lib/api-utils

Challenge CRUD/list/detail:

  • src/app/api/v1/challenges/route.ts
  • src/app/api/v1/challenges/[challengeId]/route.ts

Registration:

  • src/app/api/v1/challenges/[challengeId]/registrations/route.ts
  • src/app/api/v1/users/me/challenges/route.ts

Live scoreboard and participants:

  • src/app/api/v1/challenges/[challengeId]/scoreboard/route.ts
  • src/app/api/v1/challenges/[challengeId]/participants/route.ts

Analytics / demo processing status:

  • src/app/api/v1/challenges/[challengeId]/analytics/route.ts
  • src/app/api/v1/users/me/challenge-analytics-summaries/route.ts
  • Core server logic: src/lib/challenges/challenge-analytics.server.ts

FACEIT and sync:

  • FACEIT OAuth connect: src/app/api/v1/auth/faceit/connect/route.ts
  • FACEIT callback page: src/app/api/v1/auth/callback/faceit/page.jsx
  • Active challenge sync cron: src/app/api/v1/cron/faceit-match-sync/route.ts
  • Older manual refresh routes:
    • src/app/api/v1/challenges/[challengeId]/refresh-games/route.ts
    • src/app/api/v1/challenges/[challengeId]/refresh-games/[userId]/route.ts

Demo queue endpoint:

  • src/app/api/v1/users/me/demos/queue/route.ts
  • Access control: src/lib/challenges/challenge-access.server.ts
  • Password challenges: src/lib/challenges/challenge-password.server.ts
  • Analytics derivation: src/lib/challenges/challenge-analytics.server.ts
  • Outcome backfill: src/lib/challenges/challenge-match-outcome-backfill.ts
  • FACEIT OAuth helpers: src/lib/faceit-oauth.ts
  • FACEIT Elo: src/lib/faceit/elo.server.ts
  • FACEIT placements: src/lib/faceit/placements.server.ts
  • Challenge DTO mapper: src/lib/mappers/challenges.ts
  • Challenge schemas/OpenAPI: src/lib/schemas/challenges.ts

Core challenge tables:

  • challenges
  • user_challenges
  • challenge_faceit_matches
  • faceit_seasons

Demo pipeline / analysis:

  • demos
  • demo_pipeline
  • analyzed_demos
  • demo_player_match_stats
  • demo_player_round_stats
  • demo_rounds
  • demo_kills
  • demo_damages
  • demo_utilities

Legacy match summary table (not used for current challenge scoring):

  • user_games

Users/auth:

  • Clerk user public metadata stores FACEIT identity.
  • Supabase users table stores app user row keyed by clerk_user_id.
FaceitConnectButton
-> FACEIT OAuth browser flow
-> POST /api/v1/auth/faceit/connect
-> FACEIT token exchange + userinfo
-> Clerk publicMetadata.faceit = { guid, nickname, avatar, country, connectedAt }

Stable identity is FACEIT guid/player id. Nickname is display/helper data and can change.

/challenges or /challenges/[id]
-> useRegisterChallenge
-> POST /api/v1/challenges/{id}/registrations
-> validates Clerk user
-> requires FACEIT metadata
-> checks challenge visibility/password/window
-> ensures Supabase users row
-> inserts user_challenges
-> snapshots starting Elo if challenge already started

user_challenges.faceit stores the nickname at registration time.

Cloud Scheduler / cron secret
-> POST /api/v1/cron/faceit-match-sync
-> active challenges from challenges table
-> participants from user_challenges
-> Clerk metadata for FACEIT player ids
-> FACEIT Data API match history
-> discover eligible challenge matches
-> resolve demo URLs if available
-> create_pipeline_job RPC for new demos
-> upsert challenge_faceit_matches
-> update Elo and placement metadata
-> backfill outcomes from analyzed demos

This route is the main production mechanism for live challenge matching and demo queuing.

See ../demo-pipeline/contracts/queues.md.

create_pipeline_job RPC
-> demos row
-> demo_pipeline row
-> pgmq demo_download message
-> faceit-downloader downloads/uploads demo
-> advance_demo_pipeline_stage
-> pgmq demo_analyze message
-> analyst parses demo and writes analyzed_demos + demo_* stats

demo_generate exists, but current showrunner integration is not the primary path for challenge scoring.

GET /api/v1/challenges/{id}/scoreboard
-> challenge + scoring_config
-> user_challenges participants/Elo
-> challenge_faceit_matches
-> demos + analyzed_demos + demo_player_match_stats
-> ScoreboardRowDTO[]

Scoreboard stats come from analyzed demos only. user_games is legacy match-summary data and is intentionally not used as a challenge scoring fallback, because switching from legacy summaries to partial analyzed-demo data can make displayed match counts decrease.

GET /api/v1/challenges/{id}/analytics
-> validates registration
-> challenge_faceit_matches for current user
-> demo_pipeline status
-> analyzed_demos
-> demo_player_match_stats matched by SteamID or normalized FACEIT name
-> processing summary + totals + per-demo status

Possible analysisStatus values:

  • not_queued
  • queued
  • processing
  • failed
  • analyzed
  • no_player_stats