Serve — Returning Results
The serve phase is the last stop before the API response. Its job: get leaderboard entries into the response body as fast as possible.
There are two paths. Which one fires depends on whether the request includes custom date range parameters.
Path 1 — Precomputed (no from/to)
This is the fast path — the one that fires for every normal leaderboard page load.
servePrecomputedScores(presetId, timePeriod)
→ getPresetComputedScores()
→ SELECT from computed_scores
JOIN leaderboard_materializations
WHERE materialization.retention_state = 'current'
AND materialization.preset_id = ?
AND materialization.entity_type = ?
AND materialization.time_period = ?
ORDER BY rank ASCSingle database query. No computation. No signals table access. The response is shaped and returned.
The active preset is resolved via getActiveScoringRulesPreset() which queries scoring_presets WHERE is_active = true AND installation_id = ?. If no preset is active (shouldn’t happen — ensureDefaultPreset() runs on every score request), the hardcoded defaultScoringRuleset() fallback applies.
Path 2 — Custom date range (has from/to)
When the user picks a custom date window, precomputed scores don’t apply — the exact window may not match any of the 6 precomputed periods.
computeCustomDateRange(ctx, scoringCtx)
→ getSignalsForOrg(installationId, repos, { since: from, until: to })
→ aggregateLeaderboard(signals, rules, entityType, teamMemberships)
→ return entries (not persisted)Steps:
- Signals are fetched directly from the
signalstable, filtered to the date range aggregateLeaderboard()inlib/leaderboard/score.tsruns the scoring engine in-memory —computeScores()+buildLeaderboard()- Results are returned in the API response and not written to any table
Custom date ranges are not persisted, so repeated identical requests recompute each time. The Redis response cache (Layer 2) may serve them, but the computed_scores table never sees this data.
Entity types
The leaderboard can rank three entity types. Each uses a different aggregation strategy:
| Entity | Aggregation | Quotas applied? | Diminishing returns? |
|---|---|---|---|
contributor | Per-user, chronological | Yes | Yes |
repository | Group signals by repo, score independently | No (skipQuota: true) | No |
team | Run computeScores() per user, then sum per team | Inherited from contributor pass | Inherited |
Repository scoring skips per-user quotas and the first_activity multiplier. A user in multiple teams scores full points for each team — team scores are not split.
Contributor activity
When includeContributorActivity is set, buildContributorActivityStats() in lib/leaderboard/contributor-activity.ts enriches each leaderboard entry with daily activity breakdowns and code additions/deletions. This powers the per-contributor timeline charts and sparklines in the UI.
Response shape
{
"entries": [
{
"rank": 1,
"userId": 12345,
"userLogin": "octocat",
"avatarUrl": "https://avatars.githubusercontent.com/u/12345",
"score": 2340.5,
"breakdown": { "commit": 800, "pr_merge": 1200, "review": 300, "comment": 40.5 },
"counts": { "commit": 80, "pr_merge": 24, "review": 15, "comment": 13 },
"additions": 4500,
"deletions": 1200
}
],
"cacheStatus": "fresh",
"computedAt": "2026-06-27T10:30:00Z",
"timePeriod": "month",
"presetName": "Default",
"timeline": [
{ "date": "2026-06-01", "count": 15 },
{ "date": "2026-06-02", "count": 22 }
]
}maxDuration
Both score and recompute routes declare export const maxDuration = 300 — 5 minutes. This requires Vercel Pro. The Hobby plan caps functions at 60 seconds; a non-trivial org will time out before ingest finishes.