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
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.
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:GET /userswith 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 ausernamestring into anidso a second request can fetch that user's reputation snapshot. This function is also called from anotFound()-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 underlyingrequest()helper — seesrc/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
/users?username=Xrequest, a dedicated/users/by-username/:usernameendpoint, or whatever shapemergefi-backendalready exposes (or would need to add) for looking up exactly one user by username. This will likely require backend coordination/a corresponding change inmergefi-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./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.notFound()path for a nonexistent handle no longer requires downloading the full user table before concluding there's no match.Acceptance Criteria
/reputation/[handle]no longer issues a request for the entire, unfiltered/userscollection.notFound(), without needing to have fetched more than the minimum data required to determine the user doesn't exist.ReputationPageshouldn't need to change beyond whatever prop/shape adjustments the new lookup requires).mergefi-backendchange, and links to or describes that coordination.Additional Notes
Precise references:
src/lib/api.ts:140-155— the fullfetchReputationByUsernamefunction; the unscopedrequest<...>("/users")call at line 145, the linear.find()at line 146.src/lib/api.ts:27-37— the underlyingrequest()helper, confirmingcache: "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, anasyncServer Component that callsfetchReputationByUsername(handle, mockReputationProfiles[handle] ?? null)and immediatelynotFound()s on anullresult — confirming the full-table fetch happens on every single page view, including ones that end in a 404.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/somehandleissues a request scoped tosomehandle(e.g. asserting the mocked fetch was called with a URL containing the username as a query/path parameter) rather than a bare/userscall with no parameters; assert a mocked "not found" response from the new scoped endpoint still results innotFound()being triggered by the page.