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.
Run codecity on your machine to access locally cloned repos.
You need:
- Docker
- macOS: Docker Desktop
- Windows: Docker Desktop, or engine + WSL
- Linux:
docker
- A modern browser with WebGL2 (Chrome, Safari, Firefox, Edge)
Set CODECITY_ALLOW_LOCAL_REPOS=1 and mount each repo directory read-only:
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- Open http://localhost:8080/
- Enter a repo URL or a local path
- Explore your city!
- Same path on both sides of the
-vis easiest - Use multiple
-vflags to mount more than one directory - Private repos: clone it yourself, and make sure the folder is volume mounted
- codecity only renders git working trees:
git initfirst to render a non-git directory
Nothing to mount, and local paths stay off:
docker run --rm --init --pull=always \
-v codecity-cache:/cache \
-p 8080:8080 \
ghcr.io/thalida/codecityTips
- The URL is the view. It carries the repo, the branch, the timeline commit and what you have selected, so copying it drops someone else exactly where you are, and Back walks you out
--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 Run it yourself |
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_CACHE_BUDGET_MB |
1024 |
Ceiling for the derived caches, swept oldest-first. Clones aren't counted and aren't swept |
CODECITY_QUIET |
off | Silence disconnect and scan logs |
Booleans take 1/true/yes/on.
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: time since last touched (recent → vivid)
- Lightness: time since last touched (recent → bright)
- Fade clock: real time, so a repo nobody has touched in a year reads faded throughout, and today's edit brightens the one building you changed
- 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
- 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), on the same clock the buildings fade by
- 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.
Deleted files keep their plot and get crossed out, so a folder that's since been emptied still shows what it used to hold. Set it in the World tab under Timeline.
- 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 repo'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, npm 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, npm 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, pyright, 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.
The API documents itself at /api/docs on any running instance, live at
codecity.io/api/docs. /api/openapi.json is the
schema behind it, and what just gen-types reads.
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.
just worktree-setup fix/my-branch # the branch has to exist first
just worktree-teardown fix/my-branch # once its PR is merged- Each worktree gets its own
<slug>.localhostURL, so source-picker recents stay isolated per project in localstorage .env.localand.claude/settings.jsonare gitignored, soworktree-setupcopies yours acrossworktree-teardowntakes the docker stack and its volumes with it
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







