Explore any public GitHub repository as an interactive 3D city. Files become buildings, directories become districts, and each language lights its own skyline.
Loading mrdoob/three.js — 5,799 files, sampled to 5,000 buildings. Recorded with npm run capture:gif.
- Repo → city, deterministically. The same repository at the same commit with the same seed always produces the same city — buildings, streets, traffic, stars, and all. Share the URL and everyone sees the identical scene.
- Districts follow your directory tree. A treemap layout carves the map into districts per directory. Height ranks a file among the repository's source files, so lockfiles and generated blobs render as low depots instead of owning the skyline; colour tracks language; and every file in a folder is built to the same architectural typology, so neighbourhoods read as neighbourhoods.
- A living city. Street networks, ground and flying traffic, rooftop details, billboards, atmosphere, and particle effects — all procedurally generated from repository data.
- Explore mode. A keyboard-accessible file explorer synced to the 3D scene: select a building to inspect the file, or walk the tree to fly to its building.
- Poster capture. Export a 1920×1080 poster of any city for sharing.
- Privacy-respecting backend. A Cloudflare Worker proxies the GitHub API with strict validation, streamed response-size caps, rate limiting keyed by pseudonymous hashes, and no client-side tokens — ever.
Every city is generated purely from repository data, so a repository's language mix becomes its skyline's colour. Nothing here is hand-tuned per repo.
facebook/react — JavaScript amber and Rust magenta, with the compiler's build_hir.rs as the tallest source file |
vuejs/vue — almost entirely TypeScript, so the city reads cyan end to end |
![]() |
![]() |
| pallets/flask — Python magenta, a small dense downtown with room to breathe | HaithamAlMaamari/RepoCity — this repository rendering itself: TypeScript cyan, docs in magenta, main.ts the tallest source file |
![]() |
![]() |
Type any owner/repo to build your own. The share URL pins the commit SHA and the presentation seed, so whoever opens it sees the identical city — same buildings, same traffic, same stars.
npm install
npm run worker:types
npm run devThis builds the static assets, starts the Worker API on http://127.0.0.1:8787, and starts Vite on http://127.0.0.1:5173 (Vite proxies /api to the local Worker). Open the Vite URL and enter any owner/repo.
Requirements: Node.js 22.13+ (22.x line), npm 10+, a WebGL2-capable browser.
The Worker works anonymously against GitHub for small repositories. For large-repository testing, copy .dev.vars.example to .dev.vars and set a fine-grained, public-contents-only GitHub token. The token is read only by the local Worker and must never appear in a VITE_* variable or any browser bundle.
Browser (Vite + Three.js) Cloudflare Worker GitHub API
┌─────────────────────────┐ ┌───────────────────┐ ┌───────────┐
│ src/data validate + │ /api │ worker/ validate,│ REST │ repos, │
│ model repo ├───────►│ traverse, sample, ├───────►│ trees, │
│ src/city treemap + │ │ cache, rate-limit │ │ languages │
│ buildings │ └───────────────────┘ └───────────┘
│ src/effects streets, │
│ traffic, sky │ Deterministic per (repo, commit, seed)
│ src/explore file tree │
└─────────────────────────┘
- Every GitHub response is treated as untrusted and runs through hand-written validators on both sides of the trust boundary (
worker/github.ts,src/data/github-contract.ts). - Successful results contain a proven-complete tree and declare whether files were sampled for rendering.
- Canonical share URLs pin the immutable commit SHA plus a presentation seed, so procedural effects reproduce exactly.
The architecture decision record lives in docs/architecture/ADR-001-github-data-service.md; the security analysis in docs/security/THREAT-MODEL.md.
Two deployables, one repository. worker/ is the backend and the only thing that talks to GitHub; src/ is the browser frontend and only ever calls the same-origin API. Nothing is shared between them at runtime — the validators are deliberately written twice, once on each side of the trust boundary.
Backend — worker/ (Cloudflare Worker, ~1,200 lines)
index.ts |
request routing, rate limiting, caching, security headers |
github.ts |
GitHub REST calls, hand-written response validation, bounded traversal |
sampling.ts |
choosing which files to render when a repository is too large |
Frontend — src/ (TypeScript + Three.js, ~9,700 lines)
data/ |
ingestion from the API, contract validation, repository modeling |
city/ |
treemap layout, districts, file classification, building typologies, the shared facade shader, the selection marker |
effects/ |
streets, ground and flying traffic, billboards, atmosphere, sky, particles |
explore/ |
the file-explorer model behind Explore mode |
core/ |
camera framing and entrance, viewpoint selection, seeded randomness, URL state |
main.ts |
scene bootstrap and UI wiring |
Supporting
scripts/ |
developer capture and measurement tools; not shipped |
docs/ |
roadmap, architecture decision record, threat model |
public/, index.html |
static assets and the app shell |
npm run lint # ESLint across app, worker, and scripts
npm run typecheck # strict TS, app + worker targets
npm test # unit tests + isolated Workerd runtime suite
npm run build # typecheck + production build
npm run ci # the full release gate CI runsnpm run ci reproduces the GitHub Actions gate: lint, unit tests, the Workerd runtime suite, both TypeScript targets, a production build, a high-severity dependency audit, and a Cloudflare deployment dry run. The Workerd suite validates real Worker bindings, request signals, streamed body limits, and Cache API behavior without production credentials or external network access.
Four developer tools sit deliberately outside the gate, because they need a GPU and network access:
npm run capture <label> # render the fixture repositories to artifacts/captures/<label>
npm run measure # report what the building shader is fed, per building
npm run capture:media # regenerate the og:image and gallery stills
npm run capture:gif # re-record the README animationAll of these require npm run dev and a local Chrome. Visual changes are verified by comparing two labelled capture runs of the same repositories at pinned commits — a synthetic fixture has given the wrong answer more than once. capture:media and capture:gif exist because every image in this README is a picture of the renderer, and a hand-made screenshot silently stops being true the moment the renderer changes. capture:gif additionally needs ffmpeg on PATH.
- The browser only ever calls the same-origin RepoCity API; GitHub credentials remain Worker secrets.
- Private repositories are rejected even when the Worker credential could read them.
- Repository names, paths, API responses, and URL state are all treated as untrusted input.
See SECURITY.md for the reporting policy and docs/security/THREAT-MODEL.md for the full analysis.
npm run deploy:check
npm run deployProduction uses Cloudflare Workers Static Assets plus the same-origin ingestion Worker. Set the optional GitHub credential with npx wrangler secret put GITHUB_TOKEN — never in wrangler.jsonc. Complete traversal of very large repositories requires a Workers Paid plan; the Free plan's CPU and subrequest limits are not sufficient for the bounded fallback traversal.
Contributions are welcome — see CONTRIBUTING.md for setup, the release gate, and the two rules that matter most (determinism is a feature; GitHub is untrusted input). docs/ROADMAP.md is an honest, evidence-based list of what is still weak, ordered by impact, and each item says what "done" looks like.
MIT © Haitham Al Maamari




