codecity turns any git repo into a 3D city. It walks the file tree and git history and builds a world: directories become streets, files become buildings, every commit grows a tree, and every author a firefly.
Open any public repo at codecity.io.
To view private and local repos, follow the steps below.
You need:
- Docker
- macOS: Docker Desktop
- Windows: Docker Desktop, or engine + WSL
- Linux:
docker
- A modern browser with WebGL2 (Chrome, Safari, Firefox, Edge)
docker run --rm --init --pull=always \
-v codecity-cache:/cache \
-p 8080:8080 \
ghcr.io/thalida/codecity- Open http://localhost:8080/ to reach the Projects page
- Paste a repo URL and pick a branch, or open one from Discover
- Explore your city!
Local folders take one more step, see Local directories below.
Tips
--pull=alwayskeeps you on the latest image; drop it to pin to your cached copy- Wipe the cache:
docker volume rm codecity-cache - Port in use?
-p 8081:8080
Everything codecity reads is an env var, passed with -e:
| Variable | Default | What it does |
|---|---|---|
CODECITY_ALLOW_LOCAL_REPOS |
off | Render local folders. Needs a matching mount, see Local directories |
CODECITY_HOSTED |
off | Marks a public deployment, where a local path can never resolve. Changes the advice shown when a repo can't be reached |
CODECITY_FEATURED_REPO |
none | The repo the landing renders behind itself, and flags in Discover. Empty means no backdrop |
CODECITY_DISCOVER |
on | The Discover tab of repos worth rendering. Set off to hide it |
CODECITY_DISCOVER_FILE |
api/discover.json |
Swap in your own curated list: a JSON array of {"url", "label"} |
CODECITY_CACHE_ROOT |
/cache |
Where clones and the manifest cache live |
CODECITY_QUIET |
off | Silence disconnect and scan logs |
Booleans take 1/true/yes/on.
Local repo support is disabled by default. To enable it, set CODECITY_ALLOW_LOCAL_REPOS=1 and mount the directory read-only into the container at the same absolute path:
docker run --rm --init --pull=always \
-e CODECITY_ALLOW_LOCAL_REPOS=1 \
-v "$HOME/Documents/Repos:$HOME/Documents/Repos:ro" \
-v codecity-cache:/cache \
-p 8080:8080 \
ghcr.io/thalida/codecity- Use multiple
-vflags to mount more than one directory - codecity only renders git working trees:
git initfirst to render a non-git directory
For per-project ignores, drop a .codecityignore at the scan root, one pattern per line:
# Skip anywhere named "fixtures"
fixtures
# Skip a specific path (relative to scan root)
tests/fixtures/large-repo
# Un-ignore a default skip (! prefix overrides ALWAYS_SKIP)
!package-lock.jsonAlways ignored, even when tracked (! un-ignores them):
- VCS:
.git,.hg,.svn - JS:
node_modules,package-lock.json,yarn.lock,pnpm-lock.yaml,bun.lock,bun.lockb,deno.lock - Python:
.venv,venv,env,__pycache__,poetry.lock,uv.lock,Pipfile.lock - Rust:
target,.cargo,Cargo.lock - Go:
Gopkg.lock,go.sum - PHP:
composer.lock - Ruby:
Gemfile.lock - Elixir:
mix.lock - CocoaPods:
Podfile.lock - Nix:
flake.lock - Framework caches:
.next,.nuxt,.svelte-kit - Test / coverage:
.pytest_cache,.mypy_cache,.ruff_cache,.tox,.coverage,htmlcov - IDE / OS:
.idea,.vscode,.DS_Store - Generated artifacts:
sbom.json(CycloneDX / SPDX software bill of materials) - Vendored single-file amalgamations:
sqlite3.c,miniz.c,lua.c(one giant.cblob inlining a whole library: 100k+ lines that would otherwise render as a single skyscraper distorting every height-based visual)
Just about every aspect of the rendering is tunable in the Settings pane, opened via the gear in the left sidebar.
- Height: line count (sqrt-interp across the floor range)
- Width & depth: byte size (log-interp, square footprint)
- Hue: file extension
- Saturation: last-modified (recent → vivid)
- Lightness: last-modified (recent → bright)
- Roof border: the color the file would have if you touched it today, so the gap between the border and the faded walls is how far it has aged
- Windows: lit-pane density, plus a glow that tracks how recently the file was created (newer files glow brighter)
- Aging: older files get grime streaks and a slight lean
- Media files (images, video) render an ad-panel face on the front above the door
- Binary files (databases,
.wasm,.so, fonts, audio) become windowless data blocks sized by byte count, faced with a fingerprint of their own bytes — or, for fonts and audio, a letter set in the font and the waveform itself
- Width tier: descendant count (step function)
- Length: packed siblings + spacing
- Label: directory name painted on the asphalt
- Placement: oldest commit closest to the gem, newest at the edges
- Height: commit age (older = taller)
- Canopy width: files changed in that commit
- Color: commits-per-day (solo-day vs busy-day color blend)
- Color: per author. Each committer gets their own hue
- Scale: that author's total commit count
- Co-authored commits:
Co-authored-by:trailers parsed out of the commit message. Each distinct contributor on a commit gets their own firefly orbiting that tree, in their own color
- Root marker: floats above the root street
- Click: clears the selection and resets the view
Scrub the whole history and watch the city grow. The scene toggle flips from Live to Timeline, and a dated slider spans the repo, a tick per commit. Drag it and the city rebuilds at that commit.
Files that don't exist at that commit still get a place, set in the World tab under Timeline:
- Deleted files keep their plot and get crossed out, so a folder that's since been emptied still shows what it used to hold.
- Future files mark where a not-yet-created file will land with an ultra-low slab, tinted toward its own color. Off by default; turn it on to see the shape the city is growing toward.
- Clone or read: Remote repos clone into a local cache (current tree only); local folders are read in place.
- Scan: codecity reads only git-tracked files (
git ls-files), honoring.codecityignoreand the default skips, and records each file's created and last-modified dates plus each commit's files, authors, and date. - Stream: Packed into one manifest and streamed to the browser as it's computed: a skeleton city renders first as a placeholder, then fills in with the full scan.
- Layout: An off-main-thread pass packs the streets so nothing overlaps: directories become streets, files line up as buildings, subdirectories branch off at right angles.
- Build: Each building is sized from its file (height = lines, footprint = bytes), one tree per commit (oldest nearest the gem), a firefly per author.
- Render: Drawn with three.js (WebGL).
You need:
- Docker
- just
- Node (
just setupinstalls the app's packages with it) - python3
- uv (for
just fmtandjust gen-types)
git clone https://github.com/thalida/codecity.git
cd codecity
just setup # one-time: pre-push hooks, app packages, .env.localRegenerating the README's screenshots and demo also needs ffmpeg and webp
(brew install ffmpeg webp); nothing else does.
The pre-push hook runs the full lint + tests before pushing; bypass with git push --no-verify (Docker must be running).
| Command | What it does |
|---|---|
just setup |
one-time: pre-push hooks, app packages, .env.local |
just dev |
Vite HMR + API auto-reload at http://<slug>.localhost:<port>/ |
just url |
print this worktree's dev URL (open $(just url)) |
just test |
pytest + vitest in containers |
just lint |
ruff, eslint, prettier, and typecheck |
just gen-types |
regenerate the frontend wire types from the OpenAPI schema |
just clean |
tear down this worktree's containers and volumes |
just --list has the rest: per-suite tests, formatting, image builds, README
assets, release and deploy.
Two env files, split by whether everyone shares the values:
.envis tracked. Version pins and the deploy target, identical for everyone..env.localis yours and gitignored, seeded from.env.local.examplebyjust setup. Put your mount and your flags there andjust devpicks them up:
CODECITY_MOUNT=~/Documents/Repos # comma-separated for several
CODECITY_FEATURED_REPO=https://github.com/thalida/codecityjust dev and just run also take docker's -v and -e for a one-off, which
beat the file for that run:
just dev -v ~/Documents/Repos/myproj -e CODECITY_DISCOVER=offA mount, from either place, turns on CODECITY_ALLOW_LOCAL_REPOS; without one,
codecity is git-URL-only. Only the CODECITY_* vars reach the container, so the
Forgejo credentials in the same file stay on your machine.
.local/ is generated state (worktree ports, the compose override). Nothing in
there is hand-edited and it's always safe to delete.
- Each worktree gets its own
<slug>.localhostURL, so source-picker recents stay isolated per project in localstorage .env.localsits at the repo root, so each worktree has its own
- FastAPI on uvicorn, single process by design (the in-memory scan-root trust set in
api/security.pycan't be split across workers) - scan progress streams over Server-Sent Events (
GET /api/manifest) - API docs at
/api/docs(Scalar); raw schema at/api/openapi.json
just release v0.2.0This ships to production. The tag is the trigger for everything below.
just release:
- verifies you're on a clean
mainin sync with origin - creates an annotated tag and pushes it
Pushing the tag triggers GitHub Actions, which:
- builds a multi-arch image (linux/amd64 + linux/arm64)
- pushes to
ghcr.io/thalida/codecitywith all tag aliases - signs with cosign (keyless via OIDC)
- smoke-tests via
/api/health - creates a GitHub Release
- deploys production, see Deploy below
A release deploys itself: once the image is published, the release workflow
dispatches deploy.yml on Forgejo. It skips itself when the secrets below
aren't set.
One-time setup
- Create a Forgejo token under Settings → Applications, scoped
repository → Read and Write, everything elseNo access - Fill in
.env.local(gitignored, seeded byjust setup) withFORGEJO_HOST,FORGEJO_REPO,FORGEJO_TOKEN - Add those same three under Settings → Secrets and variables → Actions → Secrets
Redeploying without a release
just deploycosign verify \
--certificate-identity-regexp 'https://github.com/thalida/codecity/.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
ghcr.io/thalida/codecity:v0.2.0







