Skip to main content

Third-Party Code Integration

This document details every third-party library and external service integrated into Athlora, how each is used, where the integration code lives, and the design decisions behind each choice.


1. Authentication & Identity — Auth0 + jose​

Why​

Auth0 handles identity flows (sign-up, login, password reset, social providers) without storing passwords in our database. jose provides standards-compliant JWT verification on the backend.

Packages​

PackageVersionUsed in
@auth0/auth0-react^2.24.0Frontend
jose^6.2.8Backend

Frontend integration​

Provider setup — frontend/src/main.tsx wraps the app in <Auth0Provider> configured via frontend/src/utils/auth0.ts:

// frontend/src/utils/auth0.ts
auth0ProviderOptions() returns {
domain: VITE_AUTH0_DOMAIN,
clientId: VITE_AUTH0_CLIENT_ID,
cacheLocation: 'localstorage', // persists session across reloads
authorizationParams: {
audience: VITE_AUTH0_AUDIENCE,
redirect_uri: window.location.origin,
scope: 'openid profile email',
},
}

Public routes (/log/:token, /stats) bypass Auth0 entirely and render without a provider.

Token bridge — frontend/src/features/auth/Auth0TokenBridge.tsx is a state machine with five states (idle → synchronizing → ready | consent_required | onboarding | error). On authentication it:

  1. Calls PUT /api/v1/auth/me to synchronize the Auth0 profile with the local database.
  2. Checks if consent has been accepted; if not, renders ConsentGate.
  3. Loads workspace memberships; if none exist, renders ClubOnboarding.
  4. Restores the active workspace from localStorage or server metadata.
  5. Passes getAccessTokenSilently to the API client via setAccessTokenGetter().

API client token injection — frontend/src/api/client.ts acquires a token before every request. When offline, it short-circuits to prevent Auth0 background refresh from cascading 401 errors. Tokens are sent as Authorization: Bearer <token>.

Login/logout flows — frontend/src/App.tsx and frontend/src/features/auth/AuthPage.tsx use loginWithRedirect() with appState.returnTo for post-login routing, screen_hint: 'signup' for registration, and logout({ returnTo: origin }) for sign-out.

Backend integration​

JWT verification — backend/src/middleware/auth.ts uses jose's createRemoteJWKSet to fetch Auth0's .well-known/jwks.json and jwtVerify to validate the token's signature, issuer, and audience. The three-tier middleware chain:

TierMiddlewarePurpose
1verifyAuth0TokenPure JWT verification; attaches req.auth0
2resolveLocalApplicationUserLooks up users table; returns 403 if unsynchronized or deletion pending
3resolveApplicationUserJoins workspace_members; resolves workspace from X-Workspace-Id header

Auth0 Management API — backend/src/services/auth0-management.ts uses the client_credentials grant to obtain a Management API token (scopes: delete:users, create:user_tickets). The token is cached in memory and refreshed 60 seconds before expiry. Used for permanent account deletion (DELETE /api/v2/users/{auth0Id}) and password-change tickets.

Sync endpoint — PUT /api/v1/auth/me fetches the Auth0 profile from https://{domain}/userinfo using the access token, verifies sub matches, and upserts into users via ON CONFLICT (auth0_id) DO UPDATE.

Environment variables​

VariableSidePurpose
VITE_AUTH0_DOMAINFrontendAuth0 tenant domain
VITE_AUTH0_CLIENT_IDFrontendSPA application client ID
VITE_AUTH0_AUDIENCEFrontendAPI audience identifier
AUTH0_DOMAINBackendSame tenant domain for JWT issuer validation
AUTH0_AUDIENCEBackendSame audience for JWT audience validation
AUTH0_MANAGEMENT_CLIENT_IDBackendM2M application client ID
AUTH0_MANAGEMENT_CLIENT_SECRETBackendM2M application secret (never exposed to frontend)
AUTH0_PASSWORD_RETURN_URLBackendReturn URL for password-change tickets

Key files​

FilePurpose
frontend/src/utils/auth0.tsProvider configuration helper
frontend/src/main.tsxAuth0Provider mount and bootstrap
frontend/src/features/auth/Auth0TokenBridge.tsxSync state machine, workspace selection, consent gate
frontend/src/api/client.tsToken injection into HTTP requests
backend/src/middleware/auth.tsJWT verification and three-tier middleware chain
backend/src/services/auth0-management.tsManagement API token lifecycle
backend/src/controllers/auth.tsSync endpoint and password ticket creation

2. Database — pg (PostgreSQL)​

Why​

Raw pg with hand-written SQL gives full control over query shape, transactions, and migration behavior without ORM overhead. UUID primary keys and soft deletes are designed for future offline merge.

Package​

PackageVersionUsed in
pg^8.23.0Backend
pg^8.13.1E2E

Connection pooling​

backend/src/db/client.ts creates a singleton pg.Pool from DATABASE_URL. The DbExecutor type (Pick<PoolClient, 'query'>) is used throughout services for testability — tests can inject a mock executor without touching the real pool.

Migration runner​

backend/src/db/migrate.ts implements a checksum-tracked, advisory-locked migration runner:

  • Reads sequential .sql files from src/db/migrations/.
  • Computes SHA-256 checksums of line-ending-normalized content for cross-platform stability.
  • Acquires a PostgreSQL advisory lock (pg_advisory_lock(hashtext('athlora:migrations'))) to prevent concurrent runs.
  • Baselines an existing database if 0001_init.sql is the first pending migration and all six initial tables already exist.
  • Rejects modified applied migrations with a clear error.
  • Applies all pending migrations inside a single transaction.

Transaction helpers​

backend/src/db/transaction.ts provides two wrappers:

  • withTransaction(op) — standard BEGIN/COMMIT/ROLLBACK with client release and poison-connection handling.
  • withReadTransaction(op) — uses REPEATABLE READ READ ONLY isolation for aggregate queries (dashboard, statistics) to prevent dirty reads.

Key files​

FilePurpose
backend/src/db/client.tsPool creation and DbExecutor type
backend/src/db/migrate.tsChecksum-tracked migration runner
backend/src/db/transaction.tsTransaction and read-only transaction helpers
backend/src/db/row-mappers.tsSnake-case PostgreSQL rows to camelCase DTOs

3. Weather — GraySky (Keyless)​

Why​

GraySky provides current and daily weather forecasts without requiring an API key, account, or environment variable. It uses an Open-Meteo-compatible endpoint, making it zero-configuration for development and deployment.

Integration type​

Native fetch — no npm package.

Backend implementation​

backend/src/services/weather.ts (180 lines) handles the complete integration:

API call:

GET https://graysky.net/api/forecast?lat={lat}&lon={lon}
Accept: application/json
Timeout: 5 seconds (AbortSignal.timeout)

US-to-metric normalization (normalizeGraySky):

  • Requires units: "us" in the response envelope.
  • Fahrenheit → Celsius: (F - 32) * 50 / 9 / 10 with rounding.
  • Fractional rain chance → percent: value * 100 with 1 decimal.
  • inches/hour → mm/hour: inches * 25400.
  • mph → km/h: mph * 16.09344.
  • Timezone validated via Intl.DateTimeFormat.
  • Day/night detection: explicit icon suffix (-day/-night) or sunrise/sunset comparison.

Cache:

  • WeakMap<typeof fetch, Map<string, entry>> — scoped to the fetch instance for test isolation.
  • Key: "lat,lon" string.
  • TTL: 10 minutes (CACHE_MS = 10 * 60_000).
  • Max 500 locations with FIFO eviction.
  • Concurrent in-flight deduplication: stores the promise itself so multiple callers share one request.

Error handling:

  • HTTP 429/503: reads Retry-After header (numeric or HTTP-date), applies Math.max(30_000, parsedDelay) cooldown.
  • Network errors: 30-second negative cache.
  • Timeouts → 504 WEATHER_SERVICE_TIMEOUT.
  • Malformed JSON → 502 WEATHER_SERVICE_INVALID_RESPONSE.

Two endpoints:

  • GET /api/v1/events/:id/weather — event-day forecast (selects the matching day from up to 10 daily records).
  • GET /api/v1/weather/current?latitude=&longitude= — console current-weather readout.

Frontend consumption​

  • frontend/src/features/events/EventWeatherPanel.tsx — displays temperature, rain chance, wind, timezone with GraySky attribution.
  • frontend/src/features/dashboard/CoachConsole.tsx — live weather readout in the topbar, linked to GraySky.

Key files​

FilePurpose
backend/src/services/weather.tsFull integration: API call, normalization, caching, error handling
backend/src/routes/weather.tsCurrent weather endpoint
backend/src/controllers/weather.tsController wiring
frontend/src/features/events/EventWeatherPanel.tsxEvent-day forecast display
frontend/src/features/dashboard/CoachConsole.tsxConsole weather readout

4. Venue Search & Maps — Nominatim + OpenStreetMap​

Why​

Nominatim provides free, attribution-compliant venue search without API keys. OpenStreetMap provides embeddable map previews. The server-proxied boundary keeps provider credentials and request patterns off the client.

Integration type​

Native fetch — no npm package.

Backend implementation​

backend/src/services/venues.ts (84 lines) owns the provider boundary:

API call:

GET {NOMINATIM_BASE_URL}/search?q={query}&format=jsonv2&limit=5&addressdetails=0
User-Agent: {NOMINATIM_USER_AGENT}
Timeout: 5 seconds

Throttle: A global nextProviderRequestAt timestamp enforces a minimum 1-second gap between requests, complying with Nominatim's strict rate limits.

Cache:

  • Map<lowercaseQuery, { expiresAt, results }> with 5-minute TTL.
  • Query lowercased for case-insensitive caching.

Response parsing: Strict validation — each result must have a non-empty display_name and parseable finite lat/lon within valid coordinate ranges. Returns at most 5 results with { displayName, latitude, longitude }.

Errors: Timeout → 504, network error → 502, malformed response → 502. All use the standard error envelope.

Frontend usage​

  • frontend/src/features/events/EventsPage.tsx — venue search is activated by explicit button click (no keystroke autocomplete, per Nominatim policy).
  • frontend/src/features/events/VenuePreview.tsx — renders a read-only OpenStreetMap <iframe> embed with contributor attribution, saved coordinates, and an external map link fallback.

Environment variables​

VariablePurpose
NOMINATIM_BASE_URLProvider base URL (default: https://nominatim.openstreetmap.org)
NOMINATIM_USER_AGENTIdentifiable application/contact string (required by usage policy)

Key files​

FilePurpose
backend/src/services/venues.tsProvider boundary, cache, throttle, response parsing
backend/src/routes/venues.tsSearch endpoint
frontend/src/features/events/VenuePreview.tsxMap iframe and external link

5. AI Voice Assistant — Google Gemini​

Why​

Gemini Live provides real-time voice interaction with function-calling capability, enabling hands-free athlete creation during training sessions.

Package​

PackageVersionUsed in
@google/genai^2.28.0Frontend + Backend

Backend: Token broker​

backend/src/controllers/ai.ts creates a short-lived, single-use Gemini API token:

const client = new GoogleGenAI({ apiKey: GEMINI_API_KEY });
const token = await client.authTokens.create({
uses: 1,
expireTime: new Date(Date.now() + 30 * 60 * 1000).toISOString(),
});

The API key never leaves the server. The frontend receives the token name and authenticates directly with Gemini's WebSocket endpoint.

Frontend: Live transport​

1. SDK-based (primary) — frontend/src/api/geminiLiveSdk.ts:

  • AthloraGeminiSession class wraps @google/genai's ai.live.connect().
  • Default model: gemini-3.8-live, voice: Sulafat.
  • Configures responseModalities: [Modality.AUDIO], audio transcription, and function tools. Standard 3.8 Live omits unsupported thinkingConfig; GEMINI_LIVE_MODEL=rollback selects the temporary 3.1 fallback.
  • Methods: connect(), sendText(), sendAudio(), endAudioStream(), close().
  • Callbacks: onAudio, onTranscript, onTurnStart, onTurnComplete, onInterrupted, onSleepRequested, onToolCall.

frontend/src/api/geminiLive.ts is an unused legacy WebSocket transport retained for compatibility testing. It also requests gemini-3.8-live and Sulafat; the SDK wrapper is the production implementation.

Function tools​

The production SDK declares 15 non-blocking, read-only or confirmation-gated tools for page context, disciplines, athlete and coach analytics, weather, reports, athlete-draft preparation, and sleep. It never declares direct athlete creation: draft preparation requires an explicit local coach confirmation.

Audio pipeline​

Microphone (frontend/src/api/geminiMicrophone.ts):

  • getUserMedia with echo cancellation, noise suppression, auto gain.
  • AudioContext at 16kHz (Gemini input sample rate).
  • ScriptProcessorNode converts Float32 → PCM16 → Base64 chunks.
  • Routes through a muted GainNode to prevent feedback.
  • pause()/resume() stops forwarding during assistant playback.

Playback (frontend/src/api/geminiAudio.ts):

  • Decodes Base64 → PCM16 → Float32 at 24kHz (Gemini output rate).
  • Creates AudioBuffer per chunk, schedules via AudioBufferSourceNode.
  • 2ms gain envelope at chunk boundaries to prevent clicks.
  • nextStartTime cursor for gapless playback.
  • playbackGeneration counter prevents stale scheduling after clear().

System prompt​

You are Athlora, the Athlora voice assistant.
Use the available tools for every Athlora data question and action.
Never invent platform data, weather, analytics, or reports.
Prepare athlete drafts only after validation; local coach confirmation performs creation.

Audio format​

DirectionFormatSample RateEncoding
Microphone → GeminiMono PCM1616kHzBase64
Gemini → SpeakerMono PCM1624kHzBase64

Key files​

FilePurpose
backend/src/controllers/ai.tsToken broker endpoint
backend/src/routes/ai.tsRoute mounting
frontend/src/api/ai.tsToken fetch client
frontend/src/api/geminiLiveSdk.tsPrimary SDK-based session
frontend/src/api/geminiLive.tsLegacy WebSocket transport
frontend/src/api/geminiAudio.tsPCM16 audio playback engine
frontend/src/api/geminiMicrophone.tsMicrophone capture and encoding

6. Offline-First PWA — Dexie + vite-plugin-pwa​

Why​

Dexie provides a typed, promise-based IndexedDB wrapper for offline action queuing. vite-plugin-pwa generates the service worker and manifest for installability and app-shell caching.

Packages​

PackageVersionUsed in
dexie^4.4.5Frontend
vite-plugin-pwa^1.3.0Frontend

Dexie databases​

Authenticated users — frontend/src/offline/db.ts:

Database name: athlora-${userId} (per-user isolation, current schema version 2).

db.version(2).stores({
offlineActions: 'id, [status+eventId+createdAt], eventId, status',
cachedEvents: 'id, [workspaceId+id]',
cachedParticipants: 'eventId',
cachedTimeline: 'eventId',
cachedSessions: 'id, eventId',
});

Public loggers — frontend/src/offline/publicDb.ts:

Database name: athlora-public-${hash} (session-scoped, anonymous).

db.version(2).stores({
publicOfflineActions: 'id, [status+eventId+createdAt], eventId, status',
publicCachedSnapshots: 'id',
publicCachedSessions: 'id, eventId',
});

Action queue​

frontend/src/offline/actionQueue.ts provides:

FunctionDescription
enqueueAction(input, userId)Adds a pending action with crypto.randomUUID()
getPendingActions(eventId, userId)Returns pending actions in creation order via compound index
markSynced(actionId, receipt, userId)Marks synced with server receipt
markFailed(actionId, error, userId)Marks failed with error message
getQueueStatus(eventId, userId)Returns { pending, synced, failed } counts

Sync engine​

frontend/src/offline/syncEngine.ts — drainQueue(eventId, userId):

  1. Fetches all pending actions for the event in creation order.
  2. Builds a SyncBatchRequest with deviceId, eventId, and the action array.
  3. Sends POST /api/v1/sync/batch.
  4. Processes receipts: marks each action as synced (accepted/duplicate) or failed (rejected).
  5. Returns { accepted, rejected, duplicates, failed }.

drainPublicQueue(eventId, sessionToken) uses POST /api/v1/public/logger/sync/batch.

Backend batch sync​

backend/src/services/sync.ts (393 lines) processes the batch:

  • Idempotency: checks sync_action_receipts for existing action_id before processing.
  • Optimistic concurrency: edits use WHERE version = $expectedVersion.
  • Per-action isolation: each action is individually wrapped in try/catch; a single failure does not abort the batch.
  • Audit trail: every accepted, rejected, or duplicate action is recorded in sync_action_receipts.

Service worker configuration​

frontend/vite.config.ts — VitePWA plugin:

StrategyURL patternTTLNotes
NetworkOnlyWeather endpoints—Never serves stale weather
NetworkFirst/api/v1/**1hr, 5s timeoutFalls back to cache when offline
StaleWhileRevalidateGoogle Fonts stylesheets—Serves cached, revalidates in background
CacheFirstGoogle Fonts webfonts1yrStatic font files rarely change
StaleWhileRevalidateFontshare—
CacheFirst.glb 3D models1yrLarge binary assets cached aggressively

Additional config: registerType: 'autoUpdate', maximumFileSizeToCacheInBytes: 10MB, navigateFallbackDenylist: [/^\/api\//, /^\/auth\//].

Cleanup​

frontend/src/offline/cleanup.ts — Dexie.delete(dbName) purges offline data; also clears service worker caches via caches.delete().

Key files​

FilePurpose
frontend/src/offline/db.tsAuthenticated Dexie schema
frontend/src/offline/publicDb.tsPublic logger Dexie schema
frontend/src/offline/actionQueue.tsAuthenticated action queue
frontend/src/offline/publicActionQueue.tsPublic action queue
frontend/src/offline/syncEngine.tsQueue drain and batch sync
frontend/src/offline/cleanup.tsOffline data purge
frontend/src/offline/networkStatus.tsConnectivity tracker
backend/src/services/sync.tsServer-side batch processing
backend/src/routes/sync.tsBatch sync endpoint
frontend/vite.config.tsPWA manifest and service worker config

7. Realtime — Socket.IO​

Why​

Socket.IO provides reliable WebSocket communication with automatic fallback to polling, room-based event broadcasting, and middleware support for authentication.

Packages​

PackageVersionUsed in
socket.io^4.8.3Backend
socket.io-client^4.8.3Frontend

Backend​

backend/src/realtime/index.ts (159 lines):

Dynamic loading: Socket.IO is loaded via Function('specifier', 'return import(specifier)')('socket.io') to avoid static bundling issues.

Authentication: The io.use() middleware extracts socket.handshake.auth.token and calls verifyAuth0AccessToken() — the same jose-based verification used by Express routes. Failed verification disconnects the socket.

Room management:

  • Room naming: event:{eventId}.
  • event:subscribe handler validates the socket is authenticated, then calls authorizeEventSubscription(auth0Id, eventId).
  • Authorization checks: workspace membership, accepted fixture participation, or active helper grant (with a 2-hour read-only window for completed/cancelled events).

Broadcasting: notifyEventInvalidated(eventId, resources) emits realtime:invalidate to all sockets in the event room with { id, eventId, resources, occurredAt }. Resources are typed: 'event' | 'participants' | 'results' | 'timeline'.

Helper revocation: disconnectHelperFromEvent(auth0Id, eventId) removes the helper from the event room and emits realtime:access-revoked.

Frontend​

frontend/src/features/realtime/useRealtimeRoom.ts — custom hook:

  1. Lazy loading: socket.io-client is imported dynamically to keep it out of the main bundle.
  2. Connection: io(realtimeUrl, { auth: { token, workspaceId }, transports: ['websocket', 'polling'] }).
  3. Subscription: On connect, emits event:subscribe with the eventId.
  4. Invalidation protocol: Listens for realtime:invalidate and triggers an HTTP refetch — the realtime channel carries only invalidation signals, never authoritative data.
  5. Deduplication: Set<string> of received notification IDs (capped at 200).
  6. Offline handling: Disconnects when useOnlineStatus() reports offline.
  7. States: 'unavailable', 'connecting', 'connected', 'disconnected', 'error'.

Key files​

FilePurpose
backend/src/realtime/index.tsServer: auth, rooms, broadcasting, revocation
frontend/src/features/realtime/useRealtimeRoom.tsClient: connection, subscription, invalidation

8. 3D Graphics — Three.js + React Three Fiber + drei​

Why​

Three.js with React Three Fiber provides declarative 3D rendering within React. Used for the landing page cinematic experience and the anatomical injury viewer.

Packages​

PackageVersionUsed in
three^0.185.1Frontend
@react-three/fiber^8.18.0Frontend
@react-three/drei^9.122.0Frontend

Landing page cinematic stage​

frontend/src/features/landing/cinematic/PersistentWebGLStage.tsx — one lazy-loaded, decorative canvas:

Canvas config:

<Canvas
dpr={compact ? [1, 1] : [1, 1.5]}
camera={{ position: [...], fov: 35, near: 0.1, far: 65 }}
frameloop={paused ? 'never' : 'always'}
gl={{ alpha: false, antialias: true, powerPreference: 'high-performance' }}
/>

Scene components:

  1. fog — dark navy (#00070d) from distance 11 to 31.
  2. Lights — ambient cyan (#9beff8, 0.2), directional white (#d8feff, 1.65), point cyan (#087f9c, 4.5).
  3. StadiumIntro — arched tunnel with extruded panels, glowing arches (additive blending), wordmark HTML overlays, custom shader floor with cyan edge glow and portal effect.
  4. CameraRig — smoothly interpolates position and lookAt between intro and legacy camera paths.
  5. TrackWorld — procedural stadium track geometry (8 lane lines as lineLoop, surface and lanes fade in on scroll).
  6. AthleteSignals — 4 glowing spheres moving along a CatmullRomCurve3 path.
  7. PerformanceRibbon — a <Line> geometry that morphs from track outline to 3D performance graph on scroll, with <Html> data labels.
  8. FitnessTeaserGate — lazily loads the anatomy model via requestIdleCallback.

Performance: Compact DPR profile for low-end devices, reduced-motion CSS fallback, pauses frameloop when tab is hidden.

Anatomy body viewer​

frontend/src/features/fitness/BodyViewer.tsx — on-demand injury visualization:

Model: athlora-anatomy.glb (79,534 position vertices, 120,000 triangles) with a companion athlora-anatomy-map-v2.json providing per-vertex regionId and coreWeight.

Canvas:

<Canvas dpr={[1, 1.5]} camera={{ position: [0, 1.6, 6], fov: 31 }}
gl={{ antialias: true, powerPreference: 'high-performance' }} />

Custom material (frontend/src/features/fitness/anatomyMaterial.ts):

  • MeshStandardMaterial with onBeforeCompile shader injection.
  • Injects injuryColor (vec3) and injuryStrength (float) vertex attributes.
  • Fragment shader blends injury colors based on injuryStrength with emissive glow.
  • Severity mapping: Minor = #d17b00 (0.72), Moderate = #e23b00 (0.84), Severe = #dc002f (0.98).

Surface map (frontend/src/features/fitness/anatomySurfaceMap.ts):

  • attachAnatomyAttributes() adds anatomyRegion, anatomyCoreWeight, injuryColor, injuryStrength buffer attributes.
  • updateInjuryAttributes() resolves UI injury regions (body part + area + side) to vertex region IDs and updates color/strength buffers.
  • uiMappings maps {region → area → side → [regionName]}.

Controls: OrbitControls from drei with damping (0.075), constrained polar angles (0.72–2.35), zoom range 3.4–8.5. ContactShadows for grounding. Camera auto-frames the model.

Key files​

FilePurpose
frontend/src/features/landing/cinematic/PersistentWebGLStage.tsxLanding page 3D stage
frontend/src/features/landing/cinematic/StadiumIntro.tsxTunnel and arches
frontend/src/features/landing/cinematic/introTimeline.tsCamera animation curves
frontend/src/features/fitness/BodyViewer.tsxAnatomy viewer
frontend/src/features/fitness/anatomyMaterial.tsCustom shader material
frontend/src/features/fitness/anatomySurfaceMap.tsVertex-to-injury mapping

9. Charts — Custom SVG​

Why​

The project uses hand-built SVG charts rather than Chart.js to avoid a runtime dependency and maintain full control over the visual language matching the approved mockups. Chart.js was originally planned but not installed.

Implementation​

Progression chart — frontend/src/features/athletes/ProgressionChart.tsx (371 lines):

  • Fetches data via getAthleteProgression(athleteId, { year }).
  • buildChartGeometry() computes scaled x/y coordinates:
    • X-axis: date (linear time scale).
    • Y-axis: time in seconds (with 12% padding, 5 tick marks).
  • Renders as <svg viewBox="0 0 700 320"> with axis lines, dashed grid lines, <polyline> for the data line, <circle> for points (PB milestones rendered larger with white stroke), transparent hit-area for pointer tracking, and a positioned tooltip <g>.
  • Toggle between 'chart' and 'table' view modes.

Comparison chart — frontend/src/features/comparison/ComparisonPage.tsx:

  • Supports up to 5 athletes simultaneously with 5 fixed series colors.
  • Same SVG structure as progression but with multiple <g> groups per athlete.
  • Four comparison modes: athlete-club, athlete-cross-club, club-statistics, club-comparison.
  • club-statistics and club-comparison render HTML <table> elements instead of charts.

Key files​

FilePurpose
frontend/src/features/athletes/ProgressionChart.tsxSingle-athlete progression
frontend/src/features/comparison/ComparisonPage.tsxMulti-athlete and club comparison

10. QR Code — qrcode​

Package​

PackageVersionUsed in
qrcode^1.5.4Frontend
@types/qrcode^1.5.6Frontend

Implementation​

frontend/src/features/events/PublicLoggerPanel.tsx:

import { toDataURL } from 'qrcode';

void toDataURL(shareUrl, { errorCorrectionLevel: 'M', margin: 1, width: 240 })
.then((code) => { if (current) setQrCode(code); });

Generates a Base64 data URL from the public logger shareable link ({origin}/log/{token}). Rendered as <img src={qrCode}>. The QR code is shown once after link creation and cleared on revocation.


11. Security Middleware — helmet + cors​

Packages​

PackageVersionUsed in
helmet^8.3.0Backend
cors^2.8.6Backend

Configuration​

backend/src/app.ts:

app.use(helmet()); // All default security headers
app.use(cors({ origin: allowedOrigins })); // CORS_ORIGINS env var, comma-separated

11b. Club brand media — multer + AWS SDK for S3​

Why​

Coaches upload a club logo and cover image as multipart form data. Bytes are sniffed and stored outside source control in an S3-compatible object store (Cloudflare R2, AWS S3, Backblaze B2, or local MinIO).

Packages​

PackageVersionUsed in
multer^2.4.0Backend
@types/multer^2.2.0Backend (dev)
@aws-sdk/client-s3^3.1138.0Backend

Implementation​

  • backend/src/middleware/clubMediaUpload.ts — memory-storage multer with a 5 MB single-file limit (file field). Oversize files become 413 CLUB_MEDIA_TOO_LARGE; missing files become 400 CLUB_MEDIA_REQUIRED.
  • backend/src/services/mediaStorage.ts — magic-byte sniffing (PNG/JPEG/WebP only → 415 CLUB_MEDIA_TYPE_UNSUPPORTED), content-addressed keys clubs/{workspaceId}/{kind}-{sha256}.{ext}, PutObject/GetObject/DeleteObject, and publicMediaPath (either S3_PUBLIC_BASE_URL or the API /api/v1/media/clubs/... path).
  • backend/src/controllers/clubBranding.ts — replace/delete lifecycle; old objects are removed best-effort after a successful DB commit.

Environment variables​

VariableSidePurpose
S3_ENDPOINTBackendS3-compatible endpoint URL
S3_REGIONBackendObject-store region (default auto)
S3_BUCKETBackendBucket name
S3_ACCESS_KEY_IDBackendAccess key
S3_SECRET_ACCESS_KEYBackendSecret key (never exposed to frontend)
S3_PUBLIC_BASE_URLBackendOptional public CDN/base URL for media
helmet sets X-Content-Type-Options, X-Frame-Options, Strict-Transport-Security, Content-Security-Policy, Referrer-Policy, and other standard headers with secure defaults. No custom configuration.

cors parses CORS_ORIGINS (default: http://localhost:5173) and allows only listed origins. Used by both Express HTTP and Socket.IO.

Request pipeline order​

  1. helmet() — security headers
  2. cors() — CORS headers
  3. express.json() — body parsing
  4. /health — unauthenticated
  5. /api/v1 — authenticated routes
  6. notFoundHandler — 404
  7. errorHandler — catch-all

12. Fonts — Google Fonts + Fontshare​

Loaded in frontend/index.html​

FontSourceWeightsPurpose
Bebas NeueGoogle Fonts—Display/headline (landing titles)
InterGoogle Fonts400–900Primary UI body font
Space MonoGoogle Fonts400, 700Monospace (data/numbers)
Space GroteskGoogle Fonts500–7003D overlays, landing brand
SatoshiFontshare400–700Alternative sans-serif body

Token mapping​

frontend/src/styles/tokens.css:

--font-display: 'Space Grotesk', 'Inter', ...;
--font-family-base: 'Satoshi', 'Inter', ...;
--font-family-mono: 'Space Grotesk', ...;

PWA caching​

  • Google Fonts stylesheets: StaleWhileRevalidate
  • Google Fonts webfonts: CacheFirst with 1-year TTL, max 30 entries
  • Fontshare: StaleWhileRevalidate

13. Testing — Vitest + RTL + Supertest + Playwright + axe-core​

Packages​

PackageVersionUsed in
vitest^3.2.7Frontend + Backend
@vitest/coverage-v8^3.2.7Frontend + Backend
@testing-library/react^16.3.0Frontend
@testing-library/dom^10.4.1Frontend
@testing-library/jest-dom^6.9.1Frontend
@testing-library/user-event^14.6.1Frontend
supertest^7.2.2Backend
@playwright/test^1.62.1Frontend E2E
@playwright/test^1.55.0Standalone E2E
axe-core^4.13.0Frontend + E2E
@axe-core/playwright^4.13.0Frontend
@axe-core/playwright^4.10.1E2E

Vitest configuration​

Frontend — embedded in frontend/vite.config.ts:

  • Environment: jsdom, globals enabled.
  • Setup file: src/test/setup.ts (imports @testing-library/jest-dom/vitest, stubs window.matchMedia).
  • Coverage: V8 provider, json-summary reporter.

Backend — backend/vitest.config.ts:

  • Default Node.js environment.
  • Coverage: V8 provider, json-summary reporter.

Testing patterns​

Frontend (RTL): Tests cover API wrappers, feature components, shared components, hooks, and pure logic. They use vi.mock() for dependency replacement, @testing-library/user-event for realistic interactions, and accessible role queries (getByRole, getByLabelText).

Backend (Supertest): Tests cover route validation, response shapes, status codes, auth headers, ownership non-disclosure, and lifecycle state machines. They use mocked jose.jwtVerify and pg.Pool.query.

Backend integration: TEST_DATABASE_URL-gated tests exercise real PostgreSQL migrations, persistence, aggregates, cross-coach authorization, injuries, meets, sync, and public logger authorization.

E2E (Playwright): 23 spec files in e2e/tests/ running against real Auth0, backend, frontend, and PostgreSQL. Projects: auth-setup, smoke, desktop-chromium, mobile-chromium. Auth setup fills Auth0 Universal Login forms and saves browser state. Serial execution (workers: 1) with per-project unique data.

Accessibility (axe-core): @axe-core/playwright audits dashboard, roster, events, live logger, comparison, fixtures, account, and athlete detail pages against WCAG 2.0/2.1 A/AA. Fails on critical or serious violations.

Key files​

FilePurpose
frontend/vite.config.tsFrontend Vitest config
backend/vitest.config.tsBackend Vitest config
frontend/src/test/setup.tsTest setup (matchers, matchMedia stub)
e2e/playwright.config.tsE2E Playwright config
e2e/global-setup.tsDB migration + truncation before each run
e2e/tests/auth.setup.tsAuth0 Universal Login automation
e2e/tests/helpers/accessibility.tsShared axe-core wrapper

14. Documentation — Docusaurus​

Package​

PackageVersionUsed in
@docusaurus/core3.10.2Docs
@docusaurus/preset-classic3.10.2Docs

Configuration​

docs/docusaurus.config.ts:

  • Theme: classic preset with prismThemes.github (light) and prismThemes.dracula (dark).
  • respectPrefersColorScheme: true.
  • Deployed to https://athlora-deploy.pages.dev via Cloudflare Pages.

Key files​

FilePurpose
docs/docusaurus.config.tsSite configuration
docs/sidebars.tsNavigation structure
docs/docs/Source markdown pages

15. Build & Tooling​

Vite​

PackageVersionUsed in
vite^6.4.3Frontend
@vitejs/plugin-react^4.7.0Frontend

frontend/vite.config.ts configures the React plugin, PWA plugin, Vitest, and build output. Dev server on port 5173; E2E uses strict port 5174.

TypeScript​

PackageVersionUsed in
typescript~5.9.3Frontend + Backend
typescript~6.0.2Docs

Both frontend and backend use strict: true. Backend adds noUnusedLocals, noUnusedParameters, noFallthroughCasesInSwitch.

ESLint​

PackageVersionUsed in
eslint^9.39.5All packages
typescript-eslint^8.67.0All packages
eslint-plugin-react-hooks^5.2.0Frontend
eslint-plugin-react-refresh^0.4.20Frontend

Flat config format (eslint.config.js) in each package.

Other tooling​

PackageVersionPurpose
dotenv^17.4.0Backend env loading
tsx^4.23.12Backend dev server (watch mode)
hls.js1.6.14Declared dependency (reserved for future use)

Summary: Environment Variables​

VariableSideIntegration
DATABASE_URLBackendpg connection
AUTH0_DOMAINBackendJWT issuer
AUTH0_AUDIENCEBackendJWT audience
AUTH0_MANAGEMENT_CLIENT_IDBackendManagement API
AUTH0_MANAGEMENT_CLIENT_SECRETBackendManagement API
AUTH0_PASSWORD_RETURN_URLBackendPassword tickets
GEMINI_API_KEYBackendGemini token broker
CORS_ORIGINSBackendCORS + Socket.IO
NOMINATIM_BASE_URLBackendVenue search
NOMINATIM_USER_AGENTBackendVenue search
VITE_AUTH0_DOMAINFrontendAuth0 provider
VITE_AUTH0_CLIENT_IDFrontendAuth0 provider
VITE_AUTH0_AUDIENCEFrontendAuth0 provider
VITE_API_BASE_URLFrontendAPI client base
VITE_REALTIME_URLFrontendSocket.IO endpoint

AI declaration​

This document was created or updated with the assistance of OpenCode[openai/gpt-5.6-terra].