History of significant changes to the project. Versioning: Semantic Versioning (MAJOR.MINOR.PATCH)
GitHub closed the Repository.stargazers connection on every surface (GraphQL, REST, web UI) on 2026-07-23, StarMapper's sole data source for new scans. Full response across three fronts: communicate the situation, measure what still works, ship the first replacement data path.
- UI communication (
bb179cd): newsrc/lib/stargazer-notice.tssingle source of truth for the copy,StargazerNoticeModal, announcement banner with an ISSUE badge, pre-scan and landing-page warnings, empty-map notice on repos that scan clean, full rescan disabled with an explanatory tooltip (one-line revert once/if access returns). - Access probes and pivot POCs (
97e479a): 29 alive/dead probes across REST and GraphQL. Measured: engaged-audience union recovers 6-16% of star volume; leave-one-out reconstruction recovers 94% of a repo's real stargazers with 46% mappable;search(type:USER)capped at 1k retrieval;starredRepositoriescrawl costs exactly 1.00 pt/page; the admin/OAuth exemption GitHub announced is dead in practice (404, not 403, on a repo the token owns). - Engaged-audience indexing pipeline (
570a367): replaces the dead stargazers scan with the union of still-open repo-to-user channels,forks.owner,issues.author,pullRequests.author,mentionableUsers,watchers, each carrying location inline at 1pt/page. Newsrc/lib/engaged-audience.ts, newEngagedCachetable (kept separate fromstar_eventso it doesn't pollute star-based materialized views),scripts/ops/index-engaged.tswith 3 targeting modes and 4-token rotation, wired intoauto-indexandmaintenance.sh(step 9/9). Gated behindENGAGED_AUDIENCE_ENABLED, default off. 7 unit tests.
/roadmap (8006294, 5f3e06b, 68e77e6, d023106): visitors vote on StarMapper's response to the restriction across 4 options (A/C/D shipping or planned, B a genuinely open question, owner-verified OAuth dashboard). RoadmapVote model, upsert-by-hashed-IP so a repeat vote overwrites rather than appends (both the "change your mind" UX and the anti-spam mechanism). Optional opt-in name/email/message fields behind a consent modal with an explicit no-solicitation disclaimer, HTML-escaped through the vote pipeline to close an XSS path through unescaped interpolation. Two zero-dependency ASCII diagrams (before/after, and the A/B/C/D branch with per-option status) for visitors who won't read the prose. /privacy updated to disclose the opt-in contact capture.
Each vote notifies the site owner via Resend, reusing daily-digest's RESEND_API_KEY/DIGEST_EMAIL/DIGEST_FROM convention. 3f43d52 folds a weekly recap into the existing daily-digest cron instead of a dedicated admin page: a "Roadmap vote, 7 derniers jours" section appears Mondays only (new votes this week, all-time tallies, contact-info leavers), via new getWeeklyRoadmapRecap() in roadmap-vote.ts.
Follow-up to the July 2026 Upstash quota incident (5f6e66f). A three-agent audit (system, backend/API, frontend) produced a consolidated 35-item plan; every blocker, high, medium and low finding is fixed here, verified with rtk tsc (0 errors) and the full suite (1143/1143 passing, up from 1130).
Rate limiting and Redis. Redis.fromEnv() ran with the SDK default of 5 retries and no request timeout, so a stalled Redis multiplied one logical check into several billed HTTP round trips, the actual amplification path behind the incident. All three clients (proxy.ts, github-auth.ts, geo/route.ts) now share src/lib/upstash-resilience.ts (1 retry, 1.5s timeout). /api/profile/[login]/refresh had its own route-local limiter sharing the Redis prefix rl:profile-refresh with the proxy's limiter at a different window size, and two limiters on one prefix read and increment each other's counters, producing 429s neither config explains; the route-local one is gone, the proxy's 10/60m stays. src/proxy.ts gained a routeKey() helper that collapses dynamic path segments (owner/repo/login) to * before they enter a rate-limit key. Without it, ${ip}:${pathname} on /api/profile/[login] created one Redis key per distinct login an IP ever visited, and let that IP dodge its quota entirely by varying the login. A bounded in-memory cache now short-circuits requests from identifiers rejected in the last 10s, so a fully-blocked burst costs close to zero Redis commands instead of one per request. /api/watch/[owner]/[repo] moved from the unauthenticated moderate-get tier to strict-get, since it was burning the shared server GITHUB_TOKEN with no session check at all. New mcp-github tier tightens /api/mcp/{contributors,dependencies,followers} to 10/60s when no client PAT is present. /api/badge, /api/map-image and /api/geo (the public tier) get a shared 120/60s per-IP ceiling where there was none. /api/users/autocomplete gets a global 25/60s limiter protecting GitHub's shared search budget, which a per-IP-only limit couldn't. Three POST routes (dependents/.../refresh, contributors-badge-update, follower-cache) that fell through to the generic 5/60s default now have limits sized like their siblings. /api/geo's own rate check now runs before the API-key lookup instead of after, so a brute-forced key no longer probes for free.
CSP and secrets. connect-src dropped its bare wss: (now dev-only, closing the one open exfiltration channel) and the geoapify.com/nominatim.openstreetmap.org entries, both called server-side only. Added object-src 'none' and a style-src fallback for pre-CSP3 browsers alongside the existing style-src-elem/style-src-attr split. SM_TOKEN_SECRET verification now accepts an optional SM_TOKEN_SECRET_PREV for rotation (sign with primary, verify against both, two-deploy rollover). follower-cache's POST route used to return 403 outright when SM_TOKEN_SECRET was unset, unlike every sibling write route, which broke it in local dev; it now follows the same convention. The sm-token cookie gets the __Host- prefix in production (already Secure, Path=/, no Domain). CACHE_SIGN_SECRET being unset in production now logs a loud warning instead of silently degrading to unsigned cache entries.
Database. src/lib/db.ts's standard-driver branch and 17 one-shot scripts under scripts/ now route their Postgres connection string through withVerifyFullSsl() via a new shared scripts/lib/pg-pool.ts helper, since they previously connected with whatever sslmode was in DATABASE_URL (Neon's default require, which doesn't verify the server certificate). checkDbHealth() cached a failed health check at the same 5-minute TTL as a successful one; a one-second Neon blip silently dropped GitHubUser/StarEvent writes for 5 minutes with no log line. Failures now cache for 10s and log a warning. pg and @prisma/adapter-pg moved from devDependencies to dependencies, since both are imported at runtime by the standard-driver path and every script. scripts/db/db-sync-to-neon.sh now preflights the target schema's columns before the first write, failing loudly instead of aborting mid-sync.
Client. The GitHub PAT stored in localStorage (src/lib/token.ts) had a 15-day rolling TTL that reset on every read, so an actively-used browser tab never actually expired it; a 90-day absolute ceiling now applies alongside the rolling window. 8 JSON-LD <script> blocks (7 flagged plus [owner]/[repo]/layout.tsx, found missing on inspection) were dropped silently by the strict CSP for lacking the per-request nonce, fixed to match the root layout's pattern. 6 /profile/${login} hrefs now go through encodeURIComponent, matching the one spot that already did. Removed a dead preconnect to starmapper.jawg.io (server-only geocoding host, never called from the browser).
make maintenance was exhausting a single GitHub token. Steps 1 to 4 (backfill-repo-metrics, backfill-repo-languages, backfill-contributors, backfill-organic-score) read only process.env.GITHUB_TOKEN, so GITHUB_TOKEN_2/3/4 sat idle. With 2553 repos in badge_cache and 2 REST calls each (~5106 requests), step 1 alone blew past the 5000 req/hr REST ceiling on token 1, triggered a skip storm, and left step 2 waiting ~832s for the hourly reset.
New shared helper scripts/lib/github-token-pool.ts (buildTokenPool, acquireToken, makeHeaders, syncTokenFromHeaders). It always hands out the token with the most remaining capacity, decrements optimistically so concurrent callers spread across tokens before response headers land, syncs real x-ratelimit-remaining/reset after each call, and waits for the earliest reset only when every token is spent. On a 403/429 the current token is parked (remaining = 0) and the next call rotates to a fresh one.
Steps 1 to 4 now use this pool. Steps 5 to 7 (backfill-user-top-repos, backfill-languages, batch-index-followers) already had their own rotation. Effective REST ceiling goes from 5000 to 20000 req/hr on 4 tokens (~1277 req/token for the badge_cache sweep), so a full maintenance run completes in one pass without hitting the reset wall. Verified: rtk tsc clean, backfill-repo-languages --dry-run fetches with rotation and no token warnings.
d09ebda:CLAUDE.mdsaid Next.js 16.2.6,node_modules/next/package.jsonhad 16.2.11 actually installed. Corrected.
/api/stats, /api/mcp/influential and /api/mcp/organic-score were unreliable on repos with tens of thousands of stargazers (vercel/next.js and similar). Root cause: a stale Postgres planner estimate on star_event(owner, repo) (13 rows estimated vs 58,403 actual, reproduced on a prod-synced copy) pushed the planner toward a nested-loop-per-stargazer join instead of starting from the selective github_user.followers index. /api/mcp/influential had no timeout handling for this and returned a hard 500; the other two raced Vercel's function timeout with no margin, matching the reported "timeout, then fine on retry" pattern.
scripts/db/sql/create-star-event-owner-repo-stats.sql: extended statistics (ndistinct,dependencies) onstar_event(owner, repo), applied to production. Verified locally: the influential top-followers query dropped from 1073ms to 34ms on the same repo, no query rewrite needed./api/mcp/influentialnow catches Neon's statement timeout (P2010/P2024) and returns{users: [], timedOut: true}at 200, matching the pattern already used by/api/stats/.../geo-velocity.maxDuration = 30added to/api/stats,/api/mcp/influentialand/api/mcp/organic-score, matchinggeo-velocity, so the app-level catch always has time to respond instead of racing the platform's default timeout./api/stats: the location, company and cross-repo power-user queries no longer share onePromise.all. A slow power-user lookup (a niche repo whose stargazers never overlap with the cross-repopower_users_mv) was wiping out location and company data that had already resolved successfully.
The starmapper-mcp package gains five new tools covering the features added in 0.6.7 and 0.6.8 that had no MCP surface yet.
get_contributors lists the top 50 contributors of any repo with their commit count. Pass with_locations: true and the server enriches each contributor with their geocoded location from StarMapper data. Useful for building attribution lists or understanding where a project's core team is based.
get_followers returns up to 100 followers of a GitHub user, sorted by their own follower count descending. The truncated flag signals when there are more followers than the page cap. Company and location are included per follower.
get_country_stats surfaces the full country and city breakdown for a repo already indexed on StarMapper. This replaces manual calls to /api/stats from an agent context, returning all countries and the top 30 cities in a single Markdown table.
get_global_country_stats queries the country_stats_mv materialized view and returns the cumulative developer distribution by country across every indexed repo. The total stargazer count and country count appear in the header.
get_dependencies reads a repo's own dependency graph via the GitHub SBOM API and returns a table of packages with ecosystem and version. When the dependency graph is disabled on the repo, the tool returns a message with the settings URL rather than an error.
Four new API routes back these tools: /api/mcp/contributors/[owner]/[repo], /api/mcp/followers/[login], /api/mcp/country-stats, /api/mcp/dependencies/[owner]/[repo]. The contributors route accepts a ?withLocations=1 query param; the others are plain GET.
The fetchRepoDependencies function was added to src/lib/github.ts. It calls the GitHub SBOM REST endpoint, parses purl strings from externalRefs, deduplicates by ecosystem+name, and filters the root SPDX package (the repo itself). A 403 with x-ratelimit-remaining: 0 raises GitHubRateLimitError; a 403 without quota exhaustion returns {disabled: true} gracefully without throwing.
mcp/package.json bumped from 0.1.0 to 0.2.0.
get_contributors: top 50 contributors with commit count and optional location enrichment (with_locationsparam)get_followers: top 100 followers of a GitHub user sorted by influence, with company and locationget_country_stats: full country + city table for a specific indexed repoget_global_country_stats: cross-repo developer distribution by country from the MVget_dependencies: repo's own dependency graph via GitHub SBOM API, gracefuldisabledstatefetchRepoDependenciesinsrc/lib/github.ts: SBOM endpoint, purl parsing, dedup, root package filtering- 4 new MCP routes: contributors, followers, country-stats, dependencies
A new programmatic SEO dimension exposes developer concentration data per country. Each page at /devs/in/[country] (e.g. /devs/in/germany) renders an interactive globe with all geocoded developers from that country, pulled from github_user via the countryNormalized index, grouped into grid cells to stay within Vercel's function limit.
The sidebar lists the top programming languages for that country with relative bar widths, each linking back to its language map at /devs/{language}. The header breadcrumb connects back to the /devs hub. Pages include JSON-LD (Dataset + BreadcrumbList), full generateMetadata, and Next.js "use cache" with cacheTag("explore-mvs").
Sitemap is updated: country_stats_mv rows with at least 100 mapped developers are included with priority: 0.6.
The developer hub at /devs is reorganized as two tabs: "By country" (default) and "By language". Both show count badges in the tab bar. A shared search input clears on tab switch.
Country cards now display emoji flags computed dynamically from ISO 3166-1 alpha-2 codes (no static map, every country covered). The computation uses Unicode regional indicator characters: isoToFlagEmoji("DE") → 🇩🇪. Country fallback: 🌍.
Language cards display a colored dot using GitHub's canonical language palette. TypeScript gets #3178c6, Rust #dea584, JavaScript #f1e05a, and so on for all 54 languages. Fallback: neutral gray #6e7681.
The country limit was raised from 24 to 200. New helper module src/lib/devs-display.ts holds countryFlag() and LANGUAGE_COLOR to keep the client component under the 300-line threshold.
- Search input gains a clear (×) button when non-empty
- Empty state shows "No country matching «X»" with a "Clear search" link
- New route
/devs/in/[country]: interactive globe per country with top languages sidebar, JSON-LD, and sitemap entries /devshub redesigned as tabs ("By country" default, "By language" second), with count badges- Country cards: emoji flags computed dynamically from ISO 3166-1 alpha-2 codes, covers all countries
- Language cards: colored dots using GitHub canonical language colors for all 54 tracked languages
- Country limit raised from 24 to 200;
src/lib/devs-display.tsextracted for display constants - Search clear button (×) on the search input
A new page maps the people who built a repo, not just the people who starred it. Each contributor is geocoded via the same 3-tier cascade (Jawg → Geoapify → Nominatim) and rendered on a MapLibre map with the same clustering and heatmap toggle used on repo maps.
Contributors are fetched via GitHub's REST contributors endpoint, which returns contributions (commit count) per login. Locations are resolved using a GraphQL batch approach: 100 logins per request to avoid the N+1 problem on large repos. A GitHub token removes the anonymous rate limit and speeds up scans on repos with hundreds of contributors.
Dot size scales with commit count: contributors with more commits render as larger points, making the map readable at a glance without opening the panel. The popup shows "N commits" instead of "N followers" — a context: "contributors" field on each point routes the popup render logic without forking the map component.
The panel (left side, consistent with the followers map layout) lists all mapped contributors sorted by commit count. Each entry has a commit badge and a pin icon that flies the map to their location. An "Unmapped" tab lists contributors without a resolvable location. The scan auto-starts if a GitHub token is already stored, so the map fills in without a manual click on revisit.
Entry points:
/[owner]/[repo]/contributors— direct URL- Contributors column in the
/reposcommunity table (links to the page, shows count if already indexed) - "Contributors Map →" link in the announcement banner (example:
rtk-ai/rtk) - "Who built this?" card in the landing page explore section
The onboarding tour has four steps: intro (centered), scan controls, progress pill (optional, shown after scan starts), and the side panel (optional, shown when open).
- Token TTL extended from 7 to 15 days (users prompted to re-enter their PAT half as often)
src/lib/db.ts: replaced barerequire()withcreateRequire(import.meta.url), fixingReferenceError: require is not definedin ESM packages whenDATABASE_DRIVER=standardMakefilebatch-index-contributorstargets: changed baretsxtonode_modules/.bin/tsx(matchesauto-indexpattern, fixes PATH error when invoked via make)/repostable: fixed Deps/Contributors column order mismatch between<thead>and<tbody>- Announcement banner: removed "Language Atlas" link, added "Contributors Map", bumped
BANNER_IDtoannounce-contributors-v1
The Organic Score gains a fifth signal measuring the number of unique contributors relative to stars. Repositories with many stars but very few contributors are a known pattern in artificially inflated repos: star farming services produce accounts that star but never commit.
The signal is gated at 5,000 stars, matching the existing fork/star ratio gate. Below that threshold, contributor counts swing too widely on small projects, a solo CLI tool and a fake repo look statistically identical. Above the gate, the signal normalizes on a fixed scale: 50 or more contributors maps to 100, 20 to 70, 5 to 40, and 1 to roughly 10.
Contributors are fetched via GET /repos/{owner}/{repo}/contributors?per_page=1 using the Link header rel="last" page-number technique. One API call per repo, no pagination loop.
Adding the signal required redistributing weights across all five:
| Signal | Old weight | New weight |
|---|---|---|
| Fork/Star ratio | 40% | 25% |
| Watcher/Star ratio | 5% | 5% |
| Zero-follower stargazers | 55% | 45% |
| Releases cadence | 15% | 15% |
| Contributors / 1k stars | n/a | 10% |
contributorsCount is stored in badge_cache and populated by scripts/backfill/backfill-contributors.ts. Without --force, the script only processes NULL rows; passing --force re-fetches all rows regardless of existing values.
The modal was widened from max-w-lg to max-w-2xl and restructured into two columns. The left side shows the five signal rows; the right side groups activity pills (open issues, open PRs, latest release) and the Recompute button in a sidebar. Each signal row is now a card with a colored progress bar and the raw value displayed inline. Gated signals with insufficient data show a clear dash instead of an empty bar.
Four silent data-loss bugs were found and fixed in db-sync-to-neon.sh and db-sync-from-neon.sh:
github_user(to-neon):topRepos,topReposFetchedAt, andsourcewere missing from the explicit column list. Syncing to prod silently dropped those values for every user row.badge_cache(to-neon):contributorsCountwas absent from theON CONFLICT DO UPDATE SETclause. Data written locally bybackfill-contributors.tswas never propagated to prod.badge_cache(from-neon): the conflict clause only updated three fields (mappedCount,countryCount,totalCount). All other fields (organicScore,contributorsCount, the release fields, etc.) were silently discarded on every reverse sync.follower_cacheanddependents_cachewere completely absent fromdb-sync-to-neon.sh. Both tables are now included with staleness-guardWHERE EXCLUDED.x > table.xconflict clauses.
scripts/ops/maintenance.sh gains a step 3/6 for contributors backfill, which runs before organic score recomputation so the scorer always works with fresh contributor data. The MV refresh at the end of the pipeline now covers all 9 materialized views (country_language_stats_mv, language_grid_mv, and github_user_grid_mv were previously missing). The contributors step always passes --force so re-runs refresh existing counts rather than skipping already-populated rows.
make maintenance now launches an interactive CLI wizard built with @inquirer/prompts. A checkbox list presents the six maintenance steps and the sync+MV target with sensible defaults (all backfills on, sync off). After selection, the wizard asks for dry-run mode, prints a summary, waits for confirmation, then calls maintenance.sh with the appropriate --skip-* flags. make maintenance-dry and make maintenance-sync-only still bypass the wizard and invoke maintenance.sh directly.
The followers map page now shows a "Rescan" button when a cached result is loaded. Previously the map auto-loaded from follower_cache and there was no way to refresh without clearing the cache manually. The button appears next to the followers count pill and triggers a full re-scan from GitHub, updating both the map and the cache on completion.
The cache write plausibility check was relaxed from 1.1x to 5x the stored follower count. The old limit blocked browser rescans for accounts whose actual follower count had grown more than 10% since the last maintenance run.
make maintenance now supports an optional step 7 to refresh follower_cache for a configurable list of GitHub logins. Set REFRESH_FOLLOWERS in .env.local or inline to activate it:
REFRESH_FOLLOWERS=FlorianBruniaux make maintenanceA dedicated target is also available for one-off refreshes:
make refresh-follower-cache LOGINS=FlorianBruniaux # prod DB
make refresh-follower-cache-local LOGINS=FlorianBruniaux # local Dockerbatch-index-followers.ts gains a --logins flag that bypasses the github_user DB query and processes a comma-separated list of logins directly, writing compressed results to follower_cache via Prisma.
The /repos community maps table gains a filter bar and a dependents column. The filter bar provides language chip toggles (computed from the top 8 languages by count), a "Has dependents" chip, and a "Has score" chip; chips are combinable and a count badge shows the filtered result set size. A "Deps" column (hidden below lg breakpoint) links to the dependents page for repos with data and is sortable alongside the existing stars, mapped %, countries, score, and last-scan columns. The underlying GET /api/repos response now includes dependentsCount: number | null via a LEFT JOIN on dependents_cache filtered to non-expired rows.
Each row in /[owner]/[repo]/dependents gains a flag icon (red on hover) that opens a pre-filled GitHub issue for reporting an incorrect dependent. The issue template includes the library name, the reported dependent, its ecosystem, its package name, and a link to ecosyste.ms for reference.
The "Dependents Explorer" and "MCP Server" cards in the "More to Explore" section were converted from wide two-column cards to standard single-column grid cards, matching the layout of the other four cards in the section.
scripts/db/db-sync-from-neon.sh:follower_cacheanddependents_cachewere missing from the defaultTABLESlist. Runningpnpm db:sync:from-neonwould silently skip both tables. Both are now included with correct column lists andON CONFLICTupsert clauses.
Library authors can now see which repos and packages depend on their project. A new page /[owner]/[repo]/dependents lists dependent repos sorted by stars, forks, or name, with ecosystem badges (npm, PyPI, Go, Maven, Cargo, RubyGems, NuGet, etc.) and direct links to each dependent's StarMapper map.
Data source is ecosyste.ms: multi-ecosystem, no API key, ToS-clean. A lookup against packages.ecosyste.ms resolves the published package(s) for a repo; dependent repos are then fetched from repos.ecosyste.ms, capped at 500 rows (5 pages). Server-side sort returns HTTP 500 on ecosyste.ms, so sorting is done in-process via sortDependents().
Results are cached in Neon (dependents_cache, 7-day TTL, gzip+base64). A refresh route triggers a live fetch with a 1-hour cooldown. Repos with no published package store an empty result to avoid re-querying ecosyste.ms on every visit.
Feature-flagged via NEXT_PUBLIC_DEPENDENTS_ENABLED.
New files:
src/lib/dependents.ts:resolvePackages(),fetchDependentPages(),fetchDependents(),sortDependents(). Pure data layer, no framework coupling.src/app/[owner]/[repo]/dependents/page.tsx+page.client.tsx: dedicated page withgenerateMetadata, canonical URL, OG tags.src/components/dependents/dependents-table.tsx: sortable table, paginated, ecosystem badges, 1h refresh cooldown indicator.src/app/api/dependents/[owner]/[repo]/route.ts:GET, cache-first read withsort,page,per_pagequery params. 5-min CDN cache.src/app/api/dependents/[owner]/[repo]/refresh/route.ts:POST, live fetch + cache upsert, 1h cooldown.src/app/api/mcp/dependents/[owner]/[repo]/route.ts: MCP-facing endpoint returning top dependents by stars.mcp/src/tools/get_dependents.ts: 10th MCP tool. Returns a markdown table of top dependent repos.scripts/backfill/backfill-dependents.ts: backfills all repos inbadge_cachewith dependents data. Flags:--dry-run,--force,--limit,--delay-ms,--min-stars. Run:pnpm backfill:dependents:prod.
Schema change: DependentsCache model added to prisma/schema.prisma.
StarMapper now ships full Open Graph metadata, a PWA web manifest, and JSON-LD structured data sitewide. Every page generates a dynamic og:image at /api/og via @vercel/og. The root opengraph-image.tsx handles all pages that don't define their own image, while the repo map page already had its own OG image since 0.5.x. A manifest.ts registers the app name, icons, and theme colors for "Add to Home Screen" on mobile. Structured data (JSON-LD WebSite + SoftwareApplication schemas) is injected in the root layout.
New files: src/app/manifest.ts, src/app/opengraph-image.tsx, src/app/sitemap.ts, src/app/robots.ts, src/app/icon.svg.
On the /[owner]/followers page, users can now navigate to any other GitHub user's followers map without leaving the page. A compact @login / trigger in the header opens a command-palette modal that queries the GitHub Search API with a 200 ms debounce, showing up to 8 user results with avatars. Keyboard navigation (↑↓ Enter Escape) and the / global shortcut are supported.
New files:
src/components/followers-user-switcher.tsx: trigger button + modal componentsrc/app/api/users/autocomplete/route.ts:GET /api/users/autocomplete?q=proxies GitHubsearch/users, returns{ login, name, avatarUrl }[], 60 s CDN cache
starmapper-mcp is a standalone npm package that wraps StarMapper's API as an MCP (Model Context Protocol) server. Claude Code users can query any indexed repo's audience data from the terminal, trigger re-indexation, and get audience breakdowns directly in their AI conversations.
Nine tools:
get_repo_stats: total stars, geocoded count, top countries, top cities, organic score summaryget_organic_score: signal breakdown with weights, active signals, reasons, and 85.7% corpus accuracy labelget_velocity: per-country star velocity (last 30 days vs prior 60-day window) with rising / new / stable / declining labelsget_influential_stargazers: stargazers above a follower threshold (default 500, max 1,000,000), sorted by influence, capped at 50 resultsindex_repo: drives the full chunk loop from the MCP client, geocodes all stargazers, and saves the result to StarMapper's shared cachehealth_check: pings the StarMapper API and returns status and latencyget_cache_status: returns cache metadata for a repo (scanned date, mapped count, total) without transferring the full stargazer blobget_trending: returns the current trending repos from StarMapper's trending feedlist_repos: lists all repos indexed on StarMapper, ordered by last scan date
{ "mcpServers": { "starmapper": { "command": "npx", "args": ["starmapper-mcp"] } } }Set STARMAPPER_BASE_URL to point at a self-hosted instance.
GET /api/mcp/organic-score/[owner]/[repo]: full organic score signal breakdown. Recomputes signals live frombadge_cachevalues plus a real-time zero-follower query. Public.Cache-Control: public, s-maxage=300, stale-while-revalidate=600.GET /api/mcp/influential/[owner]/[repo]?minFollowers=N: influential stargazers above a follower threshold (0 to 1,000,000, default 500). Public, no auth gate. Hard-capped at 50 results.
A GitHub user's followers can now be mapped, exactly like a repo's stargazers. Every developer profile on StarMapper gains a /[owner]/followers page: an interactive map with GeoJSON clustering, a side panel listing followers sorted by influence (follower count), fly-to on click, and virtual scroll for large lists.
/[owner]/followerspage: full-screen map +FollowersPanelside panel with virtual scroll, fly-to on marker click, and a summary badge (mapped / total).- Profile page entry points: the followers count badge on
/profile/[login]is now a link to the followers map. A "Map followers" action button appears in the profile actions row. - Announcement banner + More to Explore: the site-wide banner promotes the followers map feature; the "More to Explore" section on the landing page includes a followers map card.
/api/followers-chunk: new POST endpoint. Fetches 100 followers per call via GitHub GraphQL, geocodes locations through the standard 3-tier cascade (Jawg, Geoapify, Nominatim), applies the 30-day stale cache strategy, and returnsFollowerPoint[]+unmapped[]. Distributed rate limiting via Upstash (30 req/min per IP, 300 req/h per PAT). IP rate limiting is skipped in development.useFollowersScanController: client-side hook that drives the/api/followers-chunkloop sequentially, accumulates points progressively, and surfaces quota remaining.
scripts/ops/index-followers.ts: single-user followers geocache warm-up. Drives the API loop for one login (make index-followers LOGIN=owner).scripts/ops/batch-index-followers.ts: batch geocache warm-up for all users in DB with ≥N followers (default 100). CallsfetchFollowersPage+geocodeBatchdirectly, no HTTP server required. Multi-token rotation readsGITHUB_TOKEN,GITHUB_TOKEN_2… and parks exhausted tokens on GitHub 429 instead of waiting.scripts/ops/index-repo.ts: drives the/api/chunkloop for any repo to pre-warm its geocache. Supports--base-urlfor local dev.
- Followers page sticky header: fixed map overlap caused by non-sticky header on the followers page.
- Touch targets on FollowersPanel: replaced arbitrary Tailwind values with standard spacing classes for 44px minimum touch targets.
- WCAG AA pass, 9 violations fixed. Six text inputs (
explorecity search, login/name search,profilerepo search,[owner]repo search and min-stars filter, maptop-panelusername input) were missing accessible names and now carry explicitaria-labelattributes. The min-stars<label>is now programmatically linked viahtmlFor/id. TheStarNudgepopup (role: dialog) gains keyboard dismissal via Escape. The unmapped-stargazers bottom drawer gainsrole="region",aria-label, and Escape-to-close. - Dark-mode contrast raised to WCAG AA.
text-muted-subtletoken in dark mode bumped from#848d97(4.2:1 ratio — below threshold) to#8b929a(~4.55:1). Affects secondary labels, breadcrumb separators, and stat hints across all pages.
- Metadata added to two high-traffic unindexed routes.
/trendingnow has a dedicatedlayout.tsxwith title, description, OG, Twitter card, and canonical./[owner](user scan page) hasgenerateMetadataproducing a dynamic title{owner}'s repos | StarMapperand a canonical URL. - OG / Twitter / canonical added to 6 secondary pages.
/privacy,/terms,/legal,/changelog,/sponsor, and/organic-score/calibrationnow carry full social metadata alongside their existing title and description. - JSON-LD root sanitization aligned. Root
layout.tsxnow applies.replace(/</g, "\\u003c")on the JSON-LD payload, consistent with the per-repo and profile layouts.
- Three raw
<img>tags in/explorereplaced with<Image>. Avatar thumbnails in the Top Contributors, Power Users, and Nearby Developers panels now use Next.js<Image>withloading="lazy", enabling automatic AVIF/WebP delivery via the optimizer.
- App Router SC/CC split on 3 major pages.
[owner]/[repo],/explore, and/profile/[login]were monolithic "use client" pages with no server-side rendering. Each now has a Server Component wrapper that pre-fetches the critical-path data (repo info, explore summary, profile) and passes it asinitialDatato the client component. The client-side fetch becomes a fallback (private repo, 404, network error) instead of the default path. LCP improves for all three routes; crawlers and social preview bots see real HTML content on first byte. map-style-urls.tsextracted fromtheme.ts.MAP_STYLE_DARK,MAP_STYLE_LIGHT,Theme, andMapProjectionmoved to a server-safe module with no"use client"marker.theme.tsre-exports them for backward compatibility. Prevents accidental bundle pollution if a server component ever imports map URL builders.
sanitizeErrormissing fromrefresh-grid-mvtest mock. The route importedsanitizeErrorfrom@/lib/api-helpers(added in 0.5.6) but the Vitest mock factory did not expose it. The function was added to the mock; the 1 failing test now passes (895/895).
JawgBadgecomponent made server-safe."use client"removed fromsrc/components/map/jawg-badge.tsx. The component is a static<a>tag with no hooks or browser APIs; the directive served no purpose.
- Prisma query payload reduction. Five
findUniquecalls withoutselectnow fetch only the columns actually consumed:badge-updatefetchestotalCountonly (was all 15+ columns),map-imagefetches 4 badge columns,stargazer-cache GETfetchesupdatedAtfor the fallback path and explicit 5-column select for the full row. Reduces data transferred from Neon on every scan completion and every map image generation. organic-score-statsSQL aggregate. The admin stats endpoint was loading allbadge_cacherows into Node.js memory for in-memory aggregation. Replaced with two parallel$queryRawGROUP BY queries (per-tier counts + per-bucket distribution viawidth_bucket). Memory footprint is now O(1) regardless of table size.
import-geocachebulk upsert. N+1 pattern (oneprisma.geoCache.upsertper row inside a nested loop) replaced with a single$queryRawUNNEST INSERT ... ON CONFLICT DO UPDATE per batch of 500. Same pattern asbulkUpsertUsersinuser-cache.ts. Admin dev-only route, blocked in production.- GDPR deletion atomicity.
delete-userroute wrapsstarEvent.deleteMany+gitHubUser.deleteinprisma.$transaction([...]). Prevents partial deletion if the process crashes between the two operations.
- Token modal clarity. Reframed "GitHub Access Token" as "Speed Boost: GitHub Token" to remove the auth/login connotation. Added a trust banner ("No account, no login, no signup. A GitHub token is just a speed pass for the API") with a Shield icon. Copy simplified: 60 vs 5,000 req/hr now explicit, link reads "Create a free token (zero permissions)". Modal widened to
max-w-lg. Browser-native password reveal/autofill icons suppressed via CSS. - Data freshness communication. A "Data updates when someone refreshes, not in real time." line now appears under the cache status row. Lock icon tooltips on Refresh and Full rescan buttons explain "Add a free GitHub token for faster scanning. No login needed."
- Community cache model explained. Pre-scan overlay rewritten: "Results are shared with everyone. When you scan, all future visitors see your results instantly." The 50k+ token warning now says "free token / zero permissions / no login, no signup." and "Add a free token (takes 30 sec)".
- FAQ additions. Two new entries: "Do I need to create an account?" (no, zero accounts anywhere) and "Is the data real-time?" (no, community snapshot model explained). Fixed a factual error: token storage was documented as
localStoragebut the implementation usessessionStoragewith a 30-minute TTL. - Watch Mode wording. Tour step and dock button title replace "real time" with "polls GitHub every 60 seconds" for accuracy.
- Landing one-liner. A three-step summary added below the community count: "1. Paste a repo · 2. We scan GitHub · 3. Everyone sees the map".
- Header / floating nav. Token button label changed from "Add token" to "Faster scans" when no token is set.
- Privacy page. sessionStorage token storage and 30-minute TTL documented in the cookies/storage section.
- Core Web Vitals: CLS pass on all pages. Full audit of landing, map, profile, and explore pages. Six targeted fixes.
AnnouncementBannernow renders visible by default and hides viauseEffectif previously dismissed. Eliminates the ~40px layout shift on first visit that was pushing CLS to 0.164.TopPanellocationCountwrapped inuseMemo([points])and component wrapped inReact.memo. Stops an O(n) country normalization loop from running on each keystroke in the "Find a stargazer" input.- Unmapped users drawer virtualized using scroll-based windowing with
ResizeObservercolumn detection. Pre-sort moved touseMemo. Opening the drawer on a repo with 30k+ unmapped users no longer blocks the main thread for 2-5 seconds. - Profile page map container now always rendered (height 0 when no coords) instead of conditionally mounted. Eliminates the 256px layout shift (CLS 0.931) on partial profiles (repo owners without a geocodable location).
- Profile page GitHub repos section now has a correctly-sized skeleton during the async fetch window, and the section skeleton is positioned after
NewsTimelineto match the loaded DOM order. Prevents two separate insertion-based layout shifts. - Explore page
UserListSkeletonnow rendersPAGE_SIZErows (30) with a matching pagination placeholder instead of 8 rows. On the mobile flex-col layout the left panel was growing from ~500px to ~1600px when data loaded, shifting the map column. CLS dropped from 0.179 to 0.001. NewsTimelineloading skeleton reduced from 2 × h-12 (104px) to a single h-10 row. For profiles with no news the old skeleton collapsed to a 38px empty-state text, causing a 66px layout shift.
- Guided tour. Five-page contextual tour (landing, map, explore, feeds, profile) with step-by-step overlay tooltips, keyboard navigation (arrow keys + Esc), and persistent completion state in localStorage. A TourTrigger button on each page lets users restart the tour at any time.
setViewModestale closure. The function closed over the localmapvariable frominitMapwhich could be destroyed after unmount or unset before layers were added. Switched tomapRef.current+hasInitializedRefguard to prevent null-pointer errors on fast navigation.- Scripts backfill
--forceflag.backfill-organic-scorenow accepts--forceto recompute already-scored rows, useful after signal weight changes or calibration updates.db-sync-to-neonexpands thebadge_cacheupsert to preserve nullable columns during sync.
- SEO/GEO pass.
robots.tsaddsBingbot,msnbot,Google-Extendedexplicitly.layout.tsxSoftwareApplicationdescription rewritten value-first;dateModifiedadded;speakablecssSelector extended. FAQ gains 5 new Q&As targeting high-GEO queries (fake star detection, Watch Mode, Geographic Velocity, influential stargazers, multi-repo compare).</script>injection escaping (<) applied consistently to all inline JSON-LD scripts. Sitemap adds/organic-score/calibrationand/sponsor. - Anti-AI text cleanup. Em dashes removed from all user-visible strings across 30 files (landing, map, profile, trending, FAQ, privacy).
- API parallelization.
badge-updateruns plausibility and organic score checks in parallel./devscount and top-countries queries use language materialized views. Nearby users and city queries usePromise.all.badge-updatecity scan timeout guard added. - Geocoder logging.
recordSuccess()added, Jawg/Geoapify HTTP status logged on error for faster diagnosis. Sitemap response cached for 1 hour.
- Follower filter: 1k+ and 5k+ tiers. Two new levels above the existing 500+ threshold:
vhigh(1k+, red dot) andelite(5k+, purple dot). The 500+ tier becomes orange. Applied consistently across the filter dock, timelapse, and share modal URL state. - Scan attribution. When a user saves a scan with their own GitHub PAT, the scan is now attributed to their GitHub login. A new
indexedBycolumn onStargazerCachestores the resolved login server-side.useScanControllerforwards the stored token asx-gh-token; the cache route resolves it viaGET /userwith a 3-second timeout and stores the result silently.pnpm stats:viewsgained a scan history panel showing the last 5 scans per repo with date, star count, and attribution. - GitHub star nudge. A small card in the bottom-right corner appears after 2 minutes of navigation, linking to the GitHub repo. Shows once per browser (dismissed to
localStorage), does not interrupt any flow.
- Vercel Runtime Cache L0 on geocoder. An in-memory cache layer added before the Neon GeoCache lookup. Repeated location strings within the same function invocation resolve instantly without hitting the DB.
- Trending map: ISR payload overflow.
fetchTrendingMapremoved fromuse cachescope after producing a 24 MB ISR fallback that exceeded Vercel's 19 MB limit. Endpoint switched toforce-dynamic, result set capped at 30k points. cacheComponentscompatibility. Dynamic Server Components (nonce injection, theme init script) wrapped in<Suspense>so the outer layout shell can prerender statically. Route segmentexport const dynamicconfigs removed from pages already covered by the globalcacheComponents: truesetting.
src/middleware.tsrenamed tosrc/proxy.ts(Next.js 16 reservesmiddleware.tsfor Vercel Routing Middleware; the StarMapper request proxy now lives under its correct name).- ESLint and TypeScript errors introduced during the
use cachemigration resolved.
use cachemigration (Next.js 16 PPR) —cacheComponents: trueenabled innext.config.ts. All data-fetching pages migrated fromexport const revalidate = N(time-based) to'use cache'+cacheTag+cacheLife(tag-based, on-demand). Cache invalidation is now surgical:POST /api/badge-updateinvalidates only the affected repo, the cron MV refresh invalidatestrendingandexplore-mvs,POST /api/newsinvalidates the author's feed. Three shared query libs extracted (repos-query.ts,trending-query.ts,devs-query.ts) so pages and API routes share the same cached functions.- Self-call anti-pattern removed — Five pages (
/,/repos,/trending,/devs/atlas,/devs) were making HTTPfetchrequests to their own API routes (e.g.,fetch("http://localhost:3000/api/trending/repos")). All five now call the DB lib directly, eliminating the unnecessary loopback latency. - Server-side fetch on 4 pages —
/repos,/trending,/devs,/devs/atlasmigrated fromuseEffectwaterfall fetch to async server components withinitialDataprop. Data is available on first paint with no client-side loading state. force-staticon 9 content pages — Static informational pages (/about,/privacy,/terms,/oss, etc.) markedforce-staticso they are prerendered at build time and served from the CDN edge with no runtime cost.- Hero globe: 7 map modals lazy-loaded —
StatsModal,ShareModal,AllStargazersModal,GrowthModal,BadgeModal,RateLimitedModal,RepoNotFoundModalare nowdynamic()imports. Reduces initial JS bundle parsed on page load.
- Globe: per-segment hemisphere clipping — Large landmasses (US, Europe, Africa) were disappearing abruptly when their ring centroid rotated past the orthographic terminator. Root cause: the renderer decided visibility at the ring level, so a polygon with any point on the back hemisphere was dropped entirely. Replaced with per-segment clipping: for each edge crossing the terminator, the exact boundary point is interpolated in geographic coordinates (
t = zA/(zA-zB)) and used as the clip point. Continents now fade out gradually at the globe edge. Fast paths retained for rings fully in front or fully behind.
resolveBaseUrlhelper — Extracted from inline page logic intosrc/lib/resolve-base-url.ts. Removed in subsequent refactor once the self-call pattern was eliminated.- GitHub star button — Star count badge added to the header.
- DB storage limit —
DB_STORAGE_LIMIT_MBenv var removed; hardcoded to 100 GB to match Neon sponsored plan. Removes an unnecessary configuration surface. - Test fixes —
reposroute mock aligned to$queryRaw(wasbadgeCache.findMany).Ratelimitstub fixed for TS2556 spreadunknown[].
- Environment validation —
src/env.tsadded via@t3-oss/env-nextjs. Build fails at compile time and server startup ifDATABASE_URL,GITHUB_TOKEN, orNEXT_PUBLIC_JAWGMAP_ACCESS_TOKENare missing. Prevents silent misconfiguration on new deployments (closes #5). - Trending: split endpoints —
GET /api/trending/reposandGET /api/trending/mapreplace the monolithicGET /api/trending. The repos list now renders before the map because the two fetches are independent. Map endpoint decompresses the top 5 repos (was 10), halving CPU work per request./trendingloading skeleton added vialoading.tsx. Legacy route kept as alias for one cycle.
- CSP
style-srchardening — Broadstyle-src 'unsafe-inline'replaced with CSP Level 3 split:style-src-elem 'self' 'nonce-{nonce}'(blocks<style>injection) andstyle-src-attr 'unsafe-inline'(scoped to element attributes only, required by React dynamic styles and MapLibre controls). Closes #56. - CI: SHA-pinned GitHub Actions — All three workflows (
ci.yml,audit.yml,semgrep.yml) now reference actions by full commit hash instead of mutable version tags. Eliminates supply-chain risk from tag mutation. Closes #55. - CI: monthly link checker — New
link-check.ymlworkflow runslycheemonthly againststarmapper.bruniaux.com, catching dead links and 404 regressions automatically.
page.tsxsplit: 2668 → 700 lines —src/app/[owner]/[repo]/page.tsxrefactored across 12 commits. Extracted components:StatsModal,ShareModal,AllStargazersModal,GrowthModal,BadgeModal,RateLimitOverlay,PreScanOverlay,RateLimitedModal,RepoNotFoundModal,GrowthChart. Extracted hooks:useScanController,useRepoCacheLoader,useCompareScan,useWatchMode,useTimelapse. Each extraction ships with its own unit tests.
- Light mode palette — Cold blue-gray tones replaced with warm cream (
#faf6edbackground, orange accent) across all light-mode CSS tokens inglobals.css. Hero globe adapts to the active theme. - WebGL error boundary —
StargazerMap,CountryChoropleth, andLanguageChoroplethnow render a fallback message instead of crashing when WebGL is unavailable (headless environments, some enterprise proxies). - Stale state on navigation —
AbortControlleradded to 7 async fetch effects in the map page (repo-info, stats, organic-score, compare-info, growth-data, geo-velocity, stargazer-cache check). Prevents state updates on an unmounted component when the user navigates mid-scan.useCompareScanthreadsAbortSignalinto each/api/chunkfetch. Closes #47. - Mobile profile layout — Profile page (
/profile/[login]) columns stack vertically on mobile (flex-col lg:flex-row). Action buttons (GitHub, Refresh, LinkedIn, Contact) are icon-only on mobile. Contact dropdown becomes a bottom-sheet. New reusablesrc/components/ui/tabs.tsxcomponent. - Mobile Explore tabs — Snap-scroll enabled on the Explore tab bar (
snap-x snap-mandatory). vs/star-historycomparison table — Replaced with stacked cards on mobile screens.- OG image errors surfaced — Unhandled errors in
opengraph-image.tsxare now caught and rendered as a text fallback instead of silently failing. Closes #49. - Vitals route: structured logging —
POST /api/vitalslogs structured JSON instead of a raw string; React list-key instability in Explore fixed. Closes #57, #58. postinstallprisma generate —package.jsonnow runsprisma generateon everypnpm install. Fixes the case where pnpm reorganizes the virtual store and wipes the generated.prisma/clientartifacts.
- Tests — jsdom + React Testing Library added to the Vitest setup. 14 component smoke tests for
ThemeToggleandTokenModal. 19 new tests for trending endpoints. 16 smoke tests for extracted map components. 8 tests foruseRepoCacheLoader, 4 foruseCompareScan. Total: 856 → 872 tests. src/lib/repo-cache.ts—LocalCachehelpers (loadCache,saveCache,clearCache,cacheKey) centralized from inlinepage.tsxdefinitions into a shared lib. Two sequentialuseEffectthat both calledloadCachemerged into one, removing a duplicatelocalStorageread on every page load.prisma/sql/schema-baseline.sql— Full SQL snapshot generated viaprisma migrate diff --from-empty. Combined withprisma/sql/views.sql, gives contributors a complete DB picture without enabling migration history. Closes #53.design-system/removed — Stale auto-generated spec (MASTER.md, 209 lines) removed;globals.cssdocumented as the single source of truth for tokens. Closes #54.exhaustive-depslint rule enabled —react-hooks/exhaustive-depsflipped fromofftowarn. 5 pre-existing intentional violations suppressed with inline comments explaining the rationale. Closes #48.- Deps — tailwindcss,
@upstash/redis,eslint-plugin-react-hooks,@types/node,tsxbumped. - Claude rules — 10 new
.claude/rules/files ported/adapted from methode-aristote:response-discipline,git-merge-discipline,react-performance-optimization,react-timers-cleanup,typescript-zero-errors,session-management,scripts-best-practices,file-organization,universal-rules,known-gotchas. CLAUDE.md slimmed from ~800 to 146 lines.
- Landing redesign — Hero split layout: input form on the left, live map preview on the right. Hero background replaced with an animated 3D canvas globe. New accent palette (cooler blues, sharper contrast).
/faqdedicated page replaces the inline FAQ section; a compact teaser remains on the landing with a "See all" link. - Jawg dual-token failover —
fetchAndPatchStyleauto-switches toNEXT_PUBLIC_JAWGMAP_ACCESS_TOKEN_2when the primary token returns 401/402/403/429 (Map Views limit). Transparent to users, no reload required. - Geocoder: promise queue — Sequential Nominatim calls now use a shared promise queue instead of a sleep-in-loop. Correct rate limiting without blocking the event loop; circuit breaker logic preserved.
- localStorage scan cache utility —
src/lib/scan-cache.tsadded (preparatory, not yet wired into the chunk loop). Persists scan results between page reloads for returning visitors.
- Trending page —
/trendingadded to sitemap and navigation. - Comparison page —
/vs/star-historywith structured data and UTM tracking on outbound badge/embed links. - Schema markup — Extended structured data across map, profile, and language pages. GEO optimization pass:
description,og:*,twitter:*fixed on all pages. - Server/client split on language pages — H1 and dev count now rendered server-side for crawlers.
- Phase 2 — rate limit hardening — Sliding window tightened on sensitive endpoints; Redis unavailability handled safely.
- Phase 3 — defense in depth —
$executeRawUnsafereplaced with$executeRaw + Prisma.sqlon all MV refresh paths. Admin endpoints return 404 (not 401/403) on auth failure to avoid endpoint discovery.
/repospage — ReplaceduseEffect + useStatefetch withuseSWR; stale-while-revalidate reduces perceived latency./api/map-image—stargazer_cacheSELECT restricted topointsonly (was fetching the full row includingunmapped).- Profile lookup —
ILIKEsearch onloginreplaced with anINclause to avoid a full table scan.
/repossort — Fetches all repos before sorting client-side; previous version only sorted the first page.- Header alignment — Content width unified to
max-w-7xlacross all pages. - Profile URL casing —
FlorianBruniaux(camelCase) used consistently in profile and badge URLs. - Profile duplicates — When
github_userhas multiple casing variants for the same login, the record with the most data is selected instead of throwing. - Globe on profile pages — Map centers on the user's own coordinates on load.
- Tests — 9 new test files added (user cache integration, background persistence, chunk route). Line coverage 79% → 87%.
- lucide-react — Inline SVGs replaced across all components.
- CI — Node.js 20 → 22 (required by pnpm 11);
pnpm/action-setupreadspackageManagerfrompackage.jsoninstead of hardcoded version. Semgrep false positives on JSON-LD suppressed. - Deps — Prisma 7.8, MapLibre GL 5.24, Zod 4.4, web-vitals 5.
- Repo hygiene —
graphify-out/untracked (328 files, 5.8MB of generated cache removed from git history going forward)..gitignoreextended with.pnpm-store/,.codex/,.code-review-graph/.
- Star growth timeline — "Growth" button in the Dock opens a weekly bar chart of star accumulation over time. Data comes from
star_event.starredAtviaGET /api/stats/[owner]/[repo]/growth(SQLDATE_TRUNC('week'), 5-min CDN cache). Falls back to in-memorystarredAttimestamps for repos scanned in the current session. Button is now visible for any repo with scan data, not just scans that captured timestamps in memory. - Landing page — community maps diversity —
/api/repos?diverse=truemode: fetches a 500-row pool and filters to max 3 repos per owner + min 100 stars before returning results. Prevents a single active user from filling the entire grid. - Landing page — "More to explore" section — 4-card grid below "How it works" linking to Explore, Developer profiles, Dev Maps, and Language Atlas. The surfaces were previously invisible to new visitors.
- Landing page — copy + FAQ improvements — "Shared cache" card renamed "Instant for everyone" with clearer copy. FAQ expanded from 7 to 10 questions (scan duration, token storage, open source). CTA label corrected from "Map It" to "Map Stargazers". Badge and data removal answers improved.
- PITCH.md rewrite — Full structural rewrite from "what changed recently" (ordered by version) to "what it is" (organized by product surface: Repo Map, Stats panel, Developer profiles, Explore, Dev Maps + Atlas, Chrome Extension, Integrations & embeds). All 7 surfaces documented. Previously undocumented: heatmap, timelapse, compare, SVG map image embed, GeoJSON API, Trending page.
- Chrome Extension v1.1.0 — profile button — On GitHub profile pages (
github.com/[login]), a "★ StarMapper" button is injected in the user sidebar that opensstarmapper.bruniaux.com/profile/[login]. Content scriptmatchesextended to["https://github.com/*", "https://github.com/*/*"]to cover single-segment paths.getPageContext()discriminated union (repo | profile | other) dispatches the correct button per page type. Profile button is full-width to match GitHub's sidebar style; injection targets.js-profile-editable-areathenLayout-sidebarwith a 2s floating fallback.
- Chrome Extension (Manifest V3) — "★ Map" button injected on every GitHub
/owner/repopage, opening the StarMapper map directly. Toolbar popup with the current repo + last 5 visited repos + search by slug or URL. Context menu on right-click for GitHub links. Handles GitHub SPA navigation (Turbo + bfcache) viaMutationObserver. Dark/light compatible via GitHub CSS variables.
- Extension refactor: WXT migration — Replaced Vite +
@crxjs/vite-pluginv2 (stagnant beta) with WXT v0.20.entrypoints/structure (background.ts, content.ts, popup/), build →.output/chrome-mv3/,wxt zipfor Chrome Web Store. Standalonetsconfig.json(withoutextends: .wxt/tsconfig.json) to avoid the Vite circular reference bug. - 5 extension fixes —
MutationObserverreplacessetTimeout(120ms)for button injection;pageshowhandler +e.persistedfor bfcache; recent repos saved tochrome.storage.localon click;SYSTEM_OWNERSblocklist in context menu (filters/settings,/explore, etc.); icons inpublic/icons/for WXT serving. - Full docs audit — Updated
README.md(Chrome Extension),PITCH.md+PITCH-en.md(Organic Score: 4 signals → 3, correct weights fork=40%/watcher=5%/zero-followers=55%; Watch mode, Geo velocity, Notable stargazers, Chrome Extension added),docs/ARCHITECTURE.md(version 0.4.6, Neon 100GB sponsored, +15 missing API routes, full file structure with schemas/ and extension/),PROJECT_INDEX.md+llms.txt(extension/ section),docs/organic-score-calibration.md(Final Decision corrected: fork=40%, non-fork=70%). docs/extension-publishing.md— Chrome Web Store guide: developer account, build/zip, upload process, updates, semver convention, profile button roadmap with prepared DOM selectors.tsconfig.json—extension/excluded from root compilation (WXT config is standalone inextension/tsconfig.json).
- Watch mode — "Watch" button in the Dock (visible for scanned repos with timestamps). Activates GitHub polling every 60s: compares recent stargazers against the start timestamp, detects new stars without rescanning. Display: pulsing green dot +
+N ★ · India, Germanyin real time. Stops automatically after 10 min with no new star. EndpointGET /api/watch/[owner]/[repo]?since=<ISO>: GitHub REST +countryNormalizedlookup fromgithub_user(no Nominatim calls). No DB writes,Cache-Control: no-store.
- Notable stargazers row — The Stats modal now shows the top 5 stargazers by followers as avatar chips, visible on open without switching tabs. Each chip shows the avatar, login, and follower count. A "Top N →" link switches to the full Top Stars tab. Data is available immediately from the in-memory scanned points (no additional API call).
- Geographic velocity ("📈 Rising") — New tab in the Stats modal that reveals which countries are discovering the repo right now. Compares the daily pace of the last 30 days against the historical pace from days 31–90. Four statuses:
rising(×1.5+),new(no history),stable,declining(≤0.5). Lazy loading: the request only fires when the tab is opened, once per session. EndpointGET /api/stats/[owner]/[repo]/geo-velocity, SQL query withCOUNT(*) FILTER, 5-min CDN cache.
- Deep link sharing — The Share modal now shows a "Current view" section when filters are active (country, city, company, followers, date, tier, view mode). The filtered URL is copy-able in one click and encodes all active filters as query params. Loading a shared URL restores the filter state and shows a dismissible "Shared view" overlay listing the active filters.
- Velocity indicator — The Stats modal summary row shows
+N/moin green under the star count, computed fromstarredAtalready in memory after a scan. Only appears when the data is present (recent scans with timestamps); silently absent for old caches.
- Zod body validation on all POST routes — All 7 POST routes migrated to a
defineRoute(schema, handler)wrapper. Newsrc/schemas/directory holds typed Zod v4 schemas for each route (track,vitals,recalculate-location,badge-update,chunk,news,stargazer-cache). Per-field error codes are declared directly in schemas;defineRoutesurfacesissues[0].messageverbatim so every existing error contract is preserved unchanged. Manualtypeof/ regex validation chains removed from all route handlers.getIPexported fromapi-helpersto replace a duplicate helper in the chunk route.
- GitHub Repos section on profile pages —
/profile/[login]now shows the cached top repos grid (up to 8, fromtopReposin DB). Count badge reflects the realpublicReposvalue from GitHub, not the cached repo list length. - Map a repo modal — "Map a repo" button next to the repos count badge opens a full-repo picker: fetches all public repos from GitHub (up to 500), searchable by name/description, sortable by Stars or A–Z. Clicking any repo navigates directly to its StarMapper map.
- Explore —
@usernamesearch — Searching with a leading@(e.g.@ruvnet) now works the same as without. The prefix is stripped before debouncing to the search state. - Profile — stale
topReposafter Refresh — After a manual Refresh,topReposFetchedAtis reset so the next profile load re-fetches top repos from GitHub instead of serving the outdated cache.
/changelogpage — Versioned timeline served fromCHANGELOG.mdat build time. Server Component with inline bold+code rendering without an external markdown dependency. Link added in the footer and in the announcement banner.
- Explore — O(N) timeout on dense bounding boxes — High-density areas (Singapore, etc.) could return 12k+ users in the bounding box. The JOIN on
user_repo_count_mvover 12k rows via Neon was exceeding the 10s statement timeout. Fixed by pushing thelat IS NOT NULLfilter before the JOIN and capping candidates to 500 before enrichment.
- FollowButton — wider dropdown — Width increased to
w-96with more padding to prevent RSS/JSON URL wrapping. - FollowButton — minimal mode on the subscribe page — On
/feed/[login], the dropdown was redundant (URLs already displayed).minimalprop added: direct toggle without dropdown.
- News & Announcements on profiles — Developers can publish short announcements (max 280 chars, optional link) directly on their StarMapper profile. Authentication via GitHub PAT — the same token used for scanning repos. 24h sliding cooldown per author (soft-deleted posts included in the cooldown, anti-bypass).
NewsTimelinecomponent integrated on/profile/[login]with skeleton loader, conditional "Publish" button (visible only if the stored token matches the page login). - RSS 2.0 + JSON Feed 1.1 per developer — Each profile exposes two subscribable feeds:
GET /api/feed/[login]/rss(RSS 2.0 with<atom:link>,If-Modified-Since, 304 response) andGET /api/feed/[login]/json(JSON Feed 1.1). Cached 1h CDN. Subscribe link (RSS icon + "Subscribe") displayed at the top of the News section on the profile. /feed/[login]page — Dedicated subscription page: hero with avatar + identity, subscribe card with copyable RSS/JSON URLs, full list of announcements, back link to the map profile. Accessible via the "Subscribe" link on the profile or "View all" on the timeline.NewsPublishModal— Publish modal with char counter (280), optional URL field, display of copyable feed URLs post-publication. Error handling: remaining cooldown displayed in h/min, invalid token clearly indicated.verifyPat()+ Upstash cache — GitHub PAT verification via the REST API, result cached in Upstash Redis. Raw token never stored. Graceful fallback if Redis is unavailable.- RSS subscriber tracking — Every hit on
/api/feed/[login]/rssis recorded inpage_view(type"feed_rss", slug = login). Queryable viapnpm stats:views.
- Dynamic CSP nonces — Per-request nonces on inline scripts, replacing static
unsafe-inlinein CSP. - POST route protection — HMAC session verification on all POST routes.
- Rate limit resilience — Rate limits fail safely when the Redis backend is unavailable.
- PAT cache hardening — Token verification cache security hardened. Revocation window reduced.
- News publish anti-race — Concurrency handling improved on the news publish flow.
- TokenModal — unresolved username — The modal was storing only the token, never the username. On pages other than the map (e.g.
/profile/[login]),getStoredUsername()returned"", which setisOwnertofalseand hid the "Publish" button even for the profile owner.handleSavenow resolves the login viaGET /api.github.com/userand stores it. "Verifying…" shown during verification;handleRemovealso clears the username. - Middleware — Rate limiting now correctly covers routes with dynamic segments.
- News cooldown — Cooldown window now correctly includes deleted posts.
- Organic score — Feature flag enforcement added on the refresh endpoint.
- Web Vitals — Input validation strengthened on the vitals endpoint.
feed-builders.ts— Two pure functions:buildRss20()(RSS 2.0 XML, correct CDATA,]]>split into two CDATA sections) andbuildJsonFeed()(JSON Feed 1.1 object). Build logic decoupled from routes for testability.isValidLogin()/normalizeLogin()— Helpers centralized ingithub-auth.ts, reused by all news and feed routes.
- Announcement banner — Dismissible top banner on the home page to announce new features. Dismissal stored in localStorage by
BANNER_ID; bump the ID to make it reappear for the next announcement. Home header switched tostickyto stack naturally below the banner. - banner-reminder hook —
PostToolUsehook that detects the creation of a newpage.tsxorroute.tsand reminds you to updateAnnouncementBanner.
- Explore Map button — The "Map" button was hidden (
opacity-0) for users with coordinates, and visible (grey) for those without. Inverted: button always visible and clickable for geolocated users,invisible(space preserved) for others. - Countries counter = 0 —
country_stats_mvwas created empty (nocountryNormalizedat creation time), then never refreshed after the backfill. Addedcreate:country-stats-mv/create:country-stats-mv:prodcommands to create and refresh the MV. - Repo column tooltips — Tooltips on sortable column headers were hidden behind the search bar. Positioning fixed.
starmapper-update.shscript — Meta-script that chains all backfills in sequence (repo-metrics+repo-languages). Commands:update:prod,update:local,update:local:force.backfill:repo-metrics:local/:local:forcescripts — Local (Docker) variants of the repo-metrics backfill withDATABASE_DRIVER=standard.
- Organic Score — "Organic" popularity score per repo (0–100), computed from activity signals independent of stars: forks, zero-dependency forks, watchers, open issues, open PRs. Weights: ZF 55% / forks 40% / watchers 5%. Score displayed via
OrganicScorePillfetched independently from the rest of the page. - Organic score column in repos list — Sortable column on the landing page, color-coded by tier (🟢 great / 🟡 good / 🟠 moderate / ⚫ low), with a detail modal on click (signal breakdown + StarScout comparison).
openPRsCountinBadgeCache— Separation of issues / PRs in the model (previously: mixedopenIssuesCountfield). Organic score modal displays both badges separately and as clickable links (GitHub links).- Organic score calibration — Debug tool to compare scores on a real sample (local dev only).
- Neon timeout —
stats/[owner]/[repo]was throwing a Neon timeout on large tables. Graceful fallback: returns the partial data available without crashing. - Prod backfill —
backfill-repo-metrics.tswas usingDATABASE_URL_LOCALinstead ofDATABASE_URLfor:prodcommands. Fixed +NEXT_PUBLIC_ORGANIC_SCORE_ENABLED=trueforced.
- Weight rebalancing — Two calibration passes: watcher 10%→5%, fork 70%→40%, zero-fork 25%→55%. More discriminating results on real repos.
- Methodology docs —
docs/organic-score.md: StarScout vs StarMapper comparison, normalization formula, known limitations.
- Developer profile page —
/profile/[login]: two-column layout (scrollable panel 2/3 + sticky map 1/3). Data: bio, followers, repos, languages, tracked star events, contribution by country. Partial profile if user is absent from DB (refresh triggered automatically). - Profile entry points — Click on avatar/login in map popups, in
explore/top,explore/power,explore/nearby. "View StarMapper profile →" button in the stargazer popup. - Profile: nearby developers — "Nearby developers" section on the profile page: list + pins on the map for geolocated devs within Xkm.
- Profile: contact dropdown — Dropdown menu with LinkedIn (obfuscated), email (obfuscated), GitHub links — protected against scraping.
- Profile: view tracking —
POST /api/tracktriggered on load; daily view counter per profile inpage_view. - GeoJSON API gated —
GET /api/geo/[owner]/[repo]: aggregated endpoint returning GeoJSON points from a scan, protected by HMAC API key. Usable by third-party tools. - Timelapse — Replay the star acquisition history by month/week with a speed selector. Based on
star_event.starredAt.
- Core Web Vitals audit — Multiple passes:
startTransitionaround chunk loop dispatches,useDeferredValueon the stargazers filter,useCallbackon map handlers, gating of expensive memos, lazy-loadTokenModal+SponsorsBlock,width/heighton avatars (CLS). - ETag + CDN — Two-step ETag on
stargazer-cache, optimized CDN TTL, redundant login index removed. - GeoJSON in throttled window — GeoJSON computed inside the throttled
setDatawindow to avoid blocked frames.
CircuitBreakerclass — Extracted into a reusable class. Unit tests added.- Cache refactor — Compression utilities centralized.
- Pre-open-source hardening — Secrets audit, hardened
.gitignore.
- Neon DB optimizations —
db:sync:from-neon:--repo,--limit,--tablesvariants for partial sync.SET statement_timeout=0added at the top of all DDL scripts (indexes + MVs). Prisma slow query logger enabled. - Additional MVs —
user_repo_count_mv(per-user repo count, nearby query 6s→200ms). GIN trigram index onlogin+name(ILIKE search 6s→50ms).
- Tests — CircuitBreaker suite added. Fixed stubs that were silently passing.
- SEO / a11y / perf audit — Robots, sitemap, structured data, focus management, aria labels, bundle size.
- Security — Pre-open-source hardening.
- Fix Jawg auth —
callJawg()ingeocoder.tswas sending the token only via thex-api-keyheader. Added theaccess-tokenquery param required by the dedicatedstarmapper.jawg.ioendpoint. Without this param, Jawg requests silently returned 401. - Fix geocode explore label —
/api/explore/geocodewas manually reconstructing the label usingp.city(a field that does not exist in the Jawg Places model). Replaced withfeature.properties.label, which Jawg provides natively. - Fix geocoder tests —
geocoder.test.tswas stubbingJAWGMAP_ACCESS_TOKENwhile the code readsJAWG_TOKEN_HEADER. 7 pre-existing tests were silently failing. Fixed. - Docs — Replaced all occurrences of "Pelias" with "Jawg Places" in
README.mdanddocs/ARCHITECTURE.md. Jawg Places is based on Pelias but the correct brand name is Jawg Places.
fetchAndPatchStyleconsolidation — The function existed twice: an inline version instargazer-map.tsx(30 lines, no cache) and a version inlib/map-style.ts(with cache).lib/map-style.tsis now the single source of truth. The function accepts aprojectionparameter ("mercator"|"globe", default"mercator") with a composite cache key${url}#${projection}to avoid globe/mercator collisions.- Removed obsolete style patches —
lang=enremoved from style URLs intheme.tsandstargazer-map.tsx(Jawg handles language natively). Glyphs URL patch removed (lib/map-style.ts,stargazer-map.tsx).name:fr → name:enreplacement removed (map-style.ts,stargazer-map.tsx). batch-scan.tsmigration — Geocoding endpoint migrated fromapi.jawg.iotostarmapper.jawg.io(dedicated StarMapper endpoint). Token migrated fromJAWGMAP_ACCESS_TOKENtoJAWG_TOKEN_HEADER. Addedx-api-keyheader.- CLI scripts refactor — The 10 scripts in
scripts/now usenode:util parseArgswithstrict: trueinstead of ad-hocprocess.argv.includes/getArgpatterns.parseArgsis native Node 18+, no dependency.
- Language Atlas —
/devs/atlaspage: world choropleth map showing the most popular language per country, computed from starred and contributed repos. Country detail on click (dominant language, %, number of devs). "Early preview" banner while the backfill runs. - Dev Maps by language —
/devsand/devs/[language]pages: developer map filtered by language, with a selection combobox. - Languages backfill —
backfill-languages.tsscript to populate thelanguages[]field ongithub_user.--from-cachemode: derives languages fromstar_event + badge_cachewith no GitHub API call (1.23M users in seconds). API mode: parallelizable via--token-index. country_language_stats_mvmaterialized view — (country × language) aggregation for the Atlas. Created/refreshed automatically bypnpm db:syncand the daily admin cron.
- Backfill 3× faster —
maxRepositories: 30 → 10(fewer GraphQL points consumed), default batch10 → 50, bulk UPDATE viaunnest()(1 SQL query instead of N individual ones).
- db:sync —
github_userwas going fromDO NOTHINGtoDO UPDATE: thelanguagesandlanguagesFetchedAtcolumns were never being pushed to Neon. Fixed. - db:sync — Automatic creation of
country_language_stats_mvon Neon during sync if it does not yet exist.
LANGUAGE_COLORS— Map of 24 languages to distinctive colors insrc/lib/language-colors.ts.LanguageChoropleth— MapLibre choropleth component (dynamic importssr: false).- Atlas copy — Wording "gravitate toward" and "favor" rather than "use" / "dominant" (data = affinity, not certified practice).
- HMAC session token — HttpOnly session cookie issued on each page load, verified on sensitive endpoints.
- Distributed rate limiting — Per-IP Redis sliding windows replacing per-instance in-memory counters. Survives serverless scaling. Tiers per endpoint sensitivity.
- Referer + origin verification — All sensitive endpoints validate request origin.
- Stargazer-cache write protection — Freshness and plausibility checks on cache writes.
- XSS fix — Map popup switched from
innerHTMLto DOM API construction. - CSP hardening —
unsafe-evalremoved in production. HSTS added. - Input validation — Character filtering on search parameters in the Explore tab.
- Error sanitization — Credentials stripped from server logs before they reach Vercel dashboard.
- Coordinate precision — API responses return rounded coordinates (~1km). Full precision stays in DB.
- Semgrep SAST CI — Automated OWASP/secrets scan on push and weekly.
- Find me — GitHub username saved in localStorage. First use: inline prompt. Subsequent visits: one click to fly to your own pin on the map.
- Badge button sidebar — "Badge" button in the sidebar (between History and Share) → mini-modal with live preview, selectable Markdown code, "Copy" button with feedback.
- Badge in Share modal — "README badge" section at the bottom of the Share panel.
- Explore 2-column layout — Leaderboard tabs (left) + sticky map (right), always visible. Max width increased to
max-w-7xl. - Owner repos list search + sort — Filter by name/description, 4 sort modes (stars desc/asc, A–Z, Z–A).
- Stats panel: publicRepos sort — Top users sortable by followers or public repos. CSV export behind env flag.
- Token required for rescan — Full rescan and delta refresh now require a GitHub token (lock icon displayed).
- Landing footer — "by Florian Bruniaux" and "Follow" pill buttons with portfolio/GitHub links.
- Community maps pagination — Paginated table (20 rows/page) with Prev/Next buttons. API limit increased from 50 to 200 repos.
- Client-side gzip compression — Scan data compressed client-side (Web CompressionStream, gzip+base64) before
POST /api/stargazer-cache. Fixes silent cache loss on repos with >~15k stars (raw payload ~15MB > Vercel 4.5MB limit). Payload reduced to ~800KB. - GeoNames geocache — Pre-seeding with ~51k entries (cities pop >15k + countries + ISO2/ISO3 codes). >99% hit rate on real scans.
- Geocache cleanup — 36 garbage entries deleted (#hashtags, $shell variables,
[object Object], XSS artifacts, Jinja templates). - DB portability — Conditional adapter in
db.ts:DATABASE_DRIVER=standard→@prisma/adapter-pg(Docker, Railway, Supabase); default →@prisma/adapter-neon. Self-hosting without Neon works. - DB optimizations — Indexes added on hot query paths, result caps (
take: 10_000), TTL-aware health guard.
- Antimeridian bug — Russia and other countries crossing the 180° meridian caused a triangle artifact on the choropleth map. Fix: polygon ring normalization so no adjacent vertices differ by more than 180° in longitude.
- MapLibre Web Worker CSP — Added
worker-src blob:(was blocking the MapLibre web worker → blank map on some configs). - React hydration —
localStorageaccess during SSR render caused React error #418. Fix: state initialized SSR-safe, synced viauseEffect. - Pre-scan modal race condition — The pre-scan modal briefly appeared on already-indexed repos. Fix:
cacheCheckDonestate. - Geocoder —
isGeocodeableLocationfilter extended to prefixes#$<>[{"!.
- AGPL-3.0-only license — SPDX headers on 50 source files, NOTICE file.
- API refactoring — Shared libs:
api-validation.ts,api-helpers.ts,compression.ts,compress-client.ts. Replaced 10 duplicated patterns across 15 route handlers. - Code conventions — API routes converted to const arrow functions.
interface→type.import typefor type-only imports. - Scripts —
batch-scan.ts: incremental FLUSH_EVERY writes, session-level geocoding cache, better error recovery. - Dependabot — Weekly dependency updates on main.
- Prisma 7.5 → 7.6 — Fixes 12 vulnerabilities (3 high, 8 moderate, 1 low) in the transitive dev dependency chain.
Initial public release.
- Dark / light mode: Toggle in the header with full CSS token migration.
- Collapsible mobile sidebar: Left sidebar on the map page is collapsible on mobile, with a visible close button.
- Landing page redesign: Two-column layout (form + community maps table), colorful feature highlights, FAQ.
- Community maps table: Table of already-scanned repos on the landing page, sorted by scan date, with Stars / Mapped% / Countries / Last scan columns.
- Followers filter: Slider to filter stargazers by follower count from the map control bar.
- Country and city filters: Filtering combobox in the stargazers table.
- LinkedIn sharing: Pre-share panel with editable text and clipboard copy.
- SVG badge:
/api/badge/[owner]/[repo]— shield with mapped count and country count, 6h CDN cache. - Image / Markdown export: From the scan stats.
- SEO / GEO:
robots.txt,sitemap.xml, structured FAQ, Open Graph metadata. - Explore page:
/explorelisting mapped repos with stats.
- Stargazer cache: Shared cache of complete scans (
stargazer_cachetable) — instant reload for subsequent visitors on the same repo. Limit: 100k stars. - Gzip compression: Stargazer cache data is compressed (gzip+base64), reducing payload size by ~70%.
- Skip cached users: The chunk endpoint does not re-write to DB users already present and unchanged.
- Geocache "not found": Locations that fail to geocode are cached with
lat=null/lng=null— avoids repeated API calls for the same garbage input. - Geoapify geocoding: Added Geoapify as fallback 2 (between Jawg and Nominatim), with circuit breaker.
- Invalid location filter:
isGeocodeableLocation()filters TLDs, phone prefixes, URLs, placeholder values before any API call.
- Client-side chunk loop: The browser orchestrates
POST /api/chunkcalls (100 users/call) to stay under the Vercel 10s timeout. - Shared geocache:
geocacheNeon table shared across all repos — a location geocoded once benefits all future scans. - 3-tier geocoding: Jawg (primary, circuit breaker) → Geoapify (fallback 1, circuit breaker) → Nominatim (final fallback, 1100ms/req).
- User-level cache:
github_user+star_eventtables to track users and their repos at the user level. - Token modal: Users can provide their own GitHub PAT for repos with >6k stars (unauthenticated limit).