GUESTBOOK.EXEcontact.exe
3D GITHUB VISUALIZER

</3d github visualizer>

3D GITHUB VISUALIZER

Any GitHub profile as a navigable 3D universe, in three draw calls a frame

3

draw calls for the whole scene — at 100 repositories and at 250

529 ms

to the first drawn frame on a throttled 4G link (median of 9)

0.2 ms

p95 frame work of a 16.7 ms budget, on an Apple M4 Pro

85.07 kB

gzipped JavaScript blocking first paint, with Three.js deferred

SPECIFICATIONS

ROLESOLO BUILD
YEAR2026
TYPEWEB APP
STATUSLIVE
STACKreact · three.js · webgl · javascript · vite · github-api · vercel · vitest +1 more
LINKS[live ↗][github ↗]
AVAILABILITYgithub-3d-visualizer.vercel.app

A GitHub profile is a flat list.

== WHAT IS THIS ==

A GitHub username becomes a navigable 3D universe: every repository is a sphere sized by its star count, coloured by its language, and placed by age, stars and forks. It is live at github-3d-visualizer.vercel.app, and the entire scene — nodes, wireframe shells and the full label set — draws in three calls a frame, constant from 100 repositories to 250. On cold load, before any network request, it renders a seeded 88-node galaxy, so the first ten seconds are never blank and can never rate-limit. The GitHub token sits behind a four-endpoint allowlist proxy at the edge and never reaches the browser.

the recorded entrance: a grey ambient galaxy drifting on cold load, dimming and dissolving as the fixture profile's repositories grow into place
cold load before any api call — 88 uncoloured placeholder spheres, the 'github username' prompt reading 'try torvalds', and STANDBY in the top right
@fixture loaded at 100 NODES: spheres sized by star count and coloured by language, each stamped with its language code (JS, PY, RS, GO), with the language legend open at right
the same cold-load state on a phone — the username field sits mid-screen over the drifting grey nodes, with preferences collapsed to a single bar at the bottom
the loaded profile on mobile: the scene compressed to the top half, with @fixture's 100 repos / 1.2m stars / 0.6s readout and the preferences, filter sets, export and heatmaps rows stacked below it

== </the problem> ==

A GitHub profile is a flat list. It tells you what someone built, but not how their work is shaped — which repositories are old and quietly popular, which are new experiments, which are forks of forks. The harder problem turned out to be the visitor who never types a username at all: that is the majority case, and the version of this app that existed before the audit showed them a blank white page while looking, to every automated check, entirely healthy.

== </my approach> ==

The whole scene is one InstancedMesh, so draw calls stay constant as repository count grows — three for nodes, wireframe shells and every label at 250 repositories. Labels are baked into a single canvas atlas drawn as one instanced quad and billboarded in the vertex shader, so they cost no per-frame CPU work. Axes are mapped by rank rather than by linear min-max, because stars and forks are power-law distributed and a linear map collapses the universe into one mass. Three.js is code-split out of the critical path and advertised with a build-time modulepreload, and the GitHub token lives in a serverless function behind a four-endpoint allowlist, with edge caching doing the real work of protecting the quota.

role & context

Solo build — the scene, the HUD, the proxy, the deployment, and the August 2026 audit that found what was actually broken. Much of the commit history is authored by the agent tooling used to write it; the repo says so under Limitations rather than hiding it.

== </the story> ==

You give it a GitHub username and it builds a universe out of that profile. Every repository becomes a geodesic icosahedron: its radius scales with the square root of its star count, its colour comes from its primary language, and its position encodes three axes at once — age on X, stars on Y, forks on Z. You orbit it, hover it, click into a repository, filter by language, and export what you are looking at.

The distinctive problem was never rendering the spheres. It was the empty state. A visitor who never types a username is the majority case, and this app used to show them a blank white page. So the empty state became the product: on cold load, before any network request, a seeded procedural generator streams 88 uncoloured placeholder spheres into a drifting galaxy. It works offline, it can never rate-limit, and it dims on your first keystroke before dissolving as real data arrives. The scenery is deliberately uncoloured, because language colour carries meaning here and decoration must not borrow it.

Positioning turned out to be the hard part. Stars and forks are power-law distributed — one repository with 60,000 stars and ninety under fifty is the normal shape of a profile — so a linear min-max map collapses almost every node onto the same coordinate and renders the universe as one overlapping mass with a couple of outliers stranded far away. The axes are mapped by rank instead, which is distribution-free, followed by a deterministic relaxation pass that separates anything still closer than the sum of its radii.

== </architecture> ==

The page is two layers over one warm-black ground: a HUD at z-index 10 and up, where a single layout component owns every fixed position on the page, and the scene at z-index 0. Eleven components each positioning themselves independently is what produced a preferences panel floating detached in a corner, and a canvas with no z-index at all is what painted over the header.

The scene is a single InstancedMesh, so draw calls stay constant as repository count grows: three for the entire scene at 250 repositories, covering nodes, wireframe shells and the full label set. Line geometry cannot go in an InstancedMesh, so the wireframe shells are drawn as one merged buffer rather than as N line segments. The labels are baked into a single canvas atlas, drawn as one instanced quad and billboarded in the vertex shader, so there is no per-frame CPU work for them at all.

Three.js is code-split out of the critical path and advertised to the browser with a build-time modulepreload, so it downloads beside the app rather than behind it: 85.07 kB gzip blocks first paint, 139.94 kB is deferred. Total JS did not go down — it rose slightly with the Three.js 0.185 upgrade — and the win is entirely on the critical path. Any copy implying the app got smaller would be unbacked.

GitHub is reached through a serverless function on Vercel. The token is read from the environment and attached to the outbound fetch only; it is never sent to the browser, never echoed in a response body, and appears in none of the client chunks. Because the incoming request carries no Authorization header of its own, responses are safely cacheable, and the CDN cache — not the rate limiter — is what protects the token's 5,000 requests/hour budget, since demo traffic concentrates on a handful of famous usernames. Per-IP throttling is a WAF rule at the edge rather than code in the function, so a throttled request costs no invocation at all.

== </key features> ==

A universe with three axes of meaning

Every repository is a geodesic icosahedron: radius scales with the square root of its star count, colour comes from its primary language, and position encodes age on X, stars on Y and forks on Z. A hairline wireframe shell shows the facet structure, and each node carries a billboarded monospace language code — JS, PY, RS, C++ — rather than an icon.

A cold load that is already moving

Before any network request, a seeded procedural generator streams 88 uncoloured placeholder spheres into a drifting galaxy. It works offline and can never rate-limit; it dims to 25% on your first keystroke and dissolves as real repositories grow into place.

A token proxy that is an allowlist

Four GitHub endpoints are proxied and everything else returns 403 — /api/github/user included, verified live against production with a real token behind it. The token attaches to the outbound fetch only, appears in none of the client chunks, and edge caching (s-maxage 1800, stale-while-revalidate a day) means repeat traffic on the same famous usernames costs no GitHub quota at all.

A portable scene-graph format

A profile is not the only way to fill the universe. Drop a .json file anywhere on the page or open ?scene=<url>. The reader refuses an unknown version rather than guessing, and reports every validation problem at once instead of one at a time.

Explore, filter, export

Orbit, hover, click into a repository, filter by language, and export what you are looking at as JSON, CSV, a PNG or a shareable URL. Tab cycles repositories, +/- zooms, j/k moves between control modules, and ? opens keyboard help.

== </key decisions> ==

DECISION 01

The proxy is an allowlist, not a passthrough. Four endpoint patterns are proxied and everything else returns 403 without ever reaching GitHub. Without that, /api/github/<anything> would be an open proxy authenticating with our token — including /user, which would reveal whose token it is. Query parameters are bounded and allowlisted the same way, so per_page=9999 clamps to 100 and an unknown client_secret is dropped rather than forwarded. There is one code path rather than a production-only branch: the dev server proxies the same route to GitHub unauthenticated, so the deployed path is the one local development exercises.

DECISION 02

Guard the blocking payload instead of silencing the chunk-size warning. Vite still prints its 500 kB warning for Three.js at 545.56 kB, deliberately. Tree-shaking already removes 27% of the library — its own full minified build is 750.94 kB — and what remains is WebGLRenderer and the shader library. Raising chunkSizeWarningLimit to silence the warning would mute a real regression detector, so instead a guard asserts that the blocking graph, the entry chunk plus its transitive static imports, stays under 500 kB with three absent from it.

DECISION 03

The empty state is the product, not a placeholder. Most visitors never type a username, so the first thing they see has to be the thing worth seeing. A seeded 88-node galaxy renders with no API call: it works offline, it can never rate-limit, and it is deterministic, which is also why the README's hero GIF can be regenerated from a fixture rather than putting a stranger's repository names in the documentation.

DECISION 04

Layout in the scene-graph format is optional but all-or-nothing. A producer that only knows about repositories should not have to invent 3D coordinates — omit position and size and the app computes them. A producer with a layout it cares about can pin it. It is honoured only if every node carries it, because half a layout would place some nodes at meaningful coordinates and the rest at the origin, which renders as a bug rather than as data.

DECISION 05

Quote frame work, not frames per second. On an M4 Pro the frame interval is pinned by the display, so an fps figure measures the monitor. Time spent inside the render loop is 0.2 ms at p95 against a 16.7 ms budget for 60 fps — and the same measurement file records a software-rasteriser floor of 72–78 ms per frame, which is what a machine with no GPU acceleration at all looks like. Both numbers travel with the hardware that produced them.

== </what the audit found> ==

The repository was audited in August 2026, and the audit is the reason it looks the way it does now. `npm run build` exited 0, transformed every module and printed no console errors — and the app rendered a near-blank white page. None of the causes was visible to a compiler: the canvas mounted `position: fixed; inset: 0` with no `z-index`, so it painted over the header rather than the header being missing; the renderer was created with `alpha: true` against a theme that defaulted to `prefers-color-scheme: light`, so the page background showed through as white; eleven components each declared their own fixed position; and the sphere geometry was built at radius `size` and then scaled by `size`, so rendered radius was size squared and small repositories disappeared entirely. The finding that mattered more than any single bug was that the verification itself was broken: headless Chromium has no WebGL, `canvas.getContext('webgl2')` returns null, and every automated check ever run against this project had been screenshotting an empty canvas and passing.

== </what was repaired> ==

The presentation bugs were fixed at the layer they belonged to — one z-index, an opaque renderer, and a single component that owns every fixed position on the page. The squared radius came out. A WebGL failure had been setting an error state that nothing ever rendered, so a visitor without WebGL got a silent empty page; it now prints a message. Two more gates turned out to be measuring nothing: a WebGL canvas cannot be read back with `drawImage` without `preserveDrawingBuffer`, so the first motion checks scored zero every time, and the browser suite hardcoded a port and started no server, so on a machine where another project held that port it had run end-to-end against a different application and scored that application's page as a rendered scene. The replacement test suite found three bugs on its first run, the best of them being that `"C++".toLowerCase()` is `c++` while the colour map's key is `cpp` — so C++ and C# repositories were falling through to the grey "Other" bucket, and a grey sphere among grey spheres reads as data rather than as a bug, which is exactly why nobody had noticed. Blue and indigo had survived a design-system purge by hiding inside `rgba()`, invisible to a hex-only guard. The favicon had never resolved: `index.html` referenced a scaffold file the repo never contained. Ten test reports claiming comprehensive coverage were deleted along with 27 other process documents, because there were zero tests behind them.

== </what was measured> ==

Three numbers changed the code rather than describing it. Frame *interval* turned out to be useless on fast hardware — 100 and 250 repositories both reported an identical 4.2 ms, which is the display refresh rather than the app — so the harness now records time spent inside the render loop instead, and that is why this page quotes frame work and not fps. Splitting Three.js into its own chunk is the obvious optimisation and was a regression until it was measured: a dynamic import is not requested until the chunk containing the import statement has downloaded, parsed and run, so the largest asset queued behind the two smallest and cost 944 ms to first frame on Fast 3G. A build-time `modulepreload` fixed it, and it exists only because the number was checked. And a linear min-max mapping of stars and forks collapsed the universe into one overlapping mass, because those metrics are power-law distributed — one repository with 60,000 stars and ninety under fifty is the normal shape of a profile — so the axes are mapped by rank, which is distribution-free, followed by a deterministic pass that separates anything still closer than the sum of its radii. The blocking payload landed at 85.07 kB gzip; masterplan.md records the before figure as 223.81 kB, but only the after number is reproducible from a clean build today, so only the after number is published as a metric.

== </decisions> ==

DECISION 01

The proxy is an allowlist, not a passthrough

because Four endpoint patterns are proxied and everything else returns 403 without reaching GitHub. Without that, /api/github/<anything> would be an open proxy authenticating with our token — including /user, which would reveal whose token it is. Query parameters are bounded too, so per_page=9999 clamps to 100 and an unknown client_secret is dropped rather than forwarded.

DECISION 02

Guard the blocking payload instead of silencing Vite's chunk-size warning

because Tree-shaking already removes 27% of Three.js — 545.56 kB against the 750.94 kB of its own full minified build — and the remainder is WebGLRenderer and the shader library. Raising chunkSizeWarningLimit would mute a real regression detector, so instead a guard asserts that the entry chunk plus its transitive static imports stays under 500 kB with three absent from it.

DECISION 03

The empty state is the product

because Most visitors never type a username, and this app used to show them a blank page. A seeded 88-node galaxy renders with no API call, so it works offline, can never rate-limit, and dims on the first keystroke before dissolving as real data arrives. It is deliberately uncoloured, because language colour carries meaning here and decoration must not borrow it.

DECISION 04

Scene-graph layout is optional, but all-or-nothing

because A producer that only knows about repositories should not have to invent 3D coordinates, so omitting position and size lets the app compute them. A supplied layout is honoured only if every node carries one, because half a layout would place some nodes meaningfully and the rest at the origin, which renders as a bug rather than as data.

DECISION 05

One code path for the proxy, in dev and in production

because vite.config.js proxies the same /api/github route straight to GitHub unauthenticated locally, so the deployed path is the one local development exercises. A production-only branch is a branch nobody runs until it breaks in front of a visitor.

== </where it doesn’t work> ==

Deployment routing is not covered by any test, and that is not hypothetical: Vercel resolved the proxy's `[...path].js` as a single dynamic segment rather than a catch-all, so every real two-segment call returned 404 and never reached the handler — the proxy was dead for 100% of real traffic while a one-segment probe looked healthy. Its unit tests all passed, because they exercise a handler production was never invoking; nothing in CI would catch a repeat, and the only thing that caught it was curling production. The frame-time figures come from one Apple M4 Pro, and the only other measurement available is a software-rasteriser floor of 72–78 ms per frame — there is still no number on integrated graphics, which is what the project's own motion spec asked for. On Fast 3G the first frame lands at 2665 ms, missing this project's own 2 s bar, and the unsplit build misses it too. "Share & Annotate" is local-only: there is no server, no sync, and annotations live in one browser's localStorage. Three language aliases are inert — F#, Objective-C and Shell map to keys with no colour defined, so they render grey — and much of the commit history is still authored by bot identities rather than by a person.

== </what i learned> ==

>

"It builds" is not "it works." This project exited 0 with no console errors and rendered a near-blank page for months, and the check that should have caught it was screenshotting an empty canvas — because headless Chromium has no WebGL. Verify the verification before trusting anything it says.

>

The obvious optimisation can be a regression. Splitting Three.js out of the entry chunk cost 944 ms to first frame on Fast 3G until a modulepreload was added, because a dynamic import is not even requested until the chunk containing it has downloaded, parsed and run.

>

Choose the metric that measures your app rather than the machine. Frame interval reported an identical 4.2 ms at 100 and at 250 repositories — that is the display refresh, not the renderer — so the harness records time inside the render loop instead.

>

Real data has a distribution, and it is rarely uniform. Mapping power-law stars and forks linearly renders every profile as one overlapping mass with two stranded outliers; rank-mapping the axes is distribution-free and fixes it for every profile at once.

>

Some bugs are invisible because they look like data. C++ and C# were rendering grey because "C++".toLowerCase() is not the map key — a grey sphere among grey spheres reads as a repository with an unusual language, which is why nobody had ever reported it.

next:

A deployment-routing check. The one bug that took production down for every real request was invisible to the entire test suite, because those tests exercise a handler production was never invoking — and it would still be invisible today.

== </full stack> ==

react · three.js · webgl · javascript · vite · github-api · vercel · vitest · playwright