Skip to content

Replace fetchReputationByUsername's full /users table scan with a scoped username lookup #80

Description

@chonilius

Overview

fetchReputationByUsername, the function powering every single visit to /reputation/[handle] (every public contributor profile page in the app), fetches the entire user table from the backend just to find one row by username:

export async function fetchReputationByUsername(
  username: string,
  fallback: ReputationProfile | null,
): Promise<ReputationProfile | null> {
  try {
    const users = await request<(RawUserProfile & { id: string })[]>("/users");
    const user = users.find((u) => u.username === username);
    if (!user) return fallback;
    const snapshot = await request<RawReputationSnapshot | null>(
      `/reputation/${user.id}`,
    );
    return adaptReputation(user, snapshot);
  } catch {
    return fallback;
  }
}

GET /users with no query parameters, no pagination, no filter — the entire platform's user list, downloaded and linear-scanned client-side (well, Server-Component-side) via .find(), purely to translate a username string into an id so a second request can fetch that user's reputation snapshot. This function is also called from a notFound()-triggering path (src/app/reputation/[handle]/page.tsx:14-20), meaning even a mistyped/nonexistent handle — the kind of request a crawler, a broken link, or a typo generates constantly — still pays the cost of downloading the full user table before concluding there's no match.

This doesn't scale. Per the landing page's own marketing copy, the platform already claims "341 contributors" — at that count this is a real but modest overhead; it becomes a serious, compounding cost as the user base grows, given /reputation/[handle] is: (a) a public, unauthenticated, presumably crawlable/shareable page (every contributor has an incentive to link their own profile), (b) rendered fresh on every request (cache: "no-store" is set in the underlying request() helper — see src/lib/api.ts:27-37 — so there's no HTTP-level caching softening this at all), and (c) architecturally guaranteed to keep getting slower over time as the user table grows, independent of how many people are actually viewing any given profile.

Requirements

  • Replace the "fetch everyone, filter client-side" pattern with a scoped lookup — either a query-parameterized /users?username=X request, a dedicated /users/by-username/:username endpoint, or whatever shape mergefi-backend already exposes (or would need to add) for looking up exactly one user by username. This will likely require backend coordination/a corresponding change in mergefi-backend — document that dependency explicitly in the PR rather than silently blocking on it, and consider whether a frontend-side interim mitigation (e.g. caching the full user list in-memory across requests within some short TTL, though this raises its own staleness concerns for a page meant to reflect a contributor's live reputation) is worth doing in the meantime.
  • Once the lookup is scoped, evaluate whether the subsequent /reputation/${user.id} fetch can be combined into a single request (e.g. a backend endpoint that resolves username → reputation snapshot directly, avoiding two sequential round-trips for one page render) — this is a secondary, smaller optimization on top of the primary fix, worth doing in the same pass if backend coordination is already happening.
  • Confirm the notFound() path for a nonexistent handle no longer requires downloading the full user table before concluding there's no match.

Acceptance Criteria

  • Loading /reputation/[handle] no longer issues a request for the entire, unfiltered /users collection.
  • A nonexistent handle still correctly triggers notFound(), without needing to have fetched more than the minimum data required to determine the user doesn't exist.
  • The existing behavior — successful profile load, live-vs-fallback mock behavior on fetch failure — is unchanged from the caller's perspective (ReputationPage shouldn't need to change beyond whatever prop/shape adjustments the new lookup requires).
  • The PR documents whether this required a corresponding mergefi-backend change, and links to or describes that coordination.

Additional Notes

Precise references:

  • src/lib/api.ts:140-155 — the full fetchReputationByUsername function; the unscoped request<...>("/users") call at line 145, the linear .find() at line 146.
  • src/lib/api.ts:27-37 — the underlying request() helper, confirming cache: "no-store" is always set (no HTTP caching layer softens the repeated full-table fetch across requests).
  • src/app/reputation/[handle]/page.tsx:9-20 — the only call site, an async Server Component that calls fetchReputationByUsername(handle, mockReputationProfiles[handle] ?? null) and immediately notFound()s on a null result — confirming the full-table fetch happens on every single page view, including ones that end in a 404.
  • Landing page context: src/lib/mock-data.ts:161-166 (platformStats.activeContributors: 341) — the platform's own stated current scale, for context on how large "the entire user table" already claims to be even before further growth.

Edge cases: if the eventual scoped-lookup endpoint is case-sensitive or has different normalization behavior than the current .find((u) => u.username === username) exact-match comparison, verify handle casing/URL-decoding behavior is preserved (GitHub usernames are case-insensitive for uniqueness purposes but are typically stored/displayed with their original casing — confirm the new endpoint's matching semantics don't silently 404 a handle that currently resolves via the exact-match .find()).

Test/reproduction plan: with a mocked request(), assert that resolving /reputation/somehandle issues a request scoped to somehandle (e.g. asserting the mocked fetch was called with a URL containing the username as a query/path parameter) rather than a bare /users call with no parameters; assert a mocked "not found" response from the new scoped endpoint still results in notFound() being triggered by the page.

Metadata

Metadata

Assignees

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardOfficial Campaign | FWC26Campaign: Official Campaign | FWC26Third CampaignCampaign: Third CampaignarchitectureArchitecture/design issueperformancePerformance/optimization issuevery hardVery difficult task, expert-level effort required

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions