▍ humdrum codex / custard
license AGPL-3.0

custard

A self-hostable, public-facing web code forge for your own git server. custard reads bare git repositories directly off disk (via go-git) and renders its own pages — repo browser, syntax-highlighted files, commit log + per-file diffs, branches/tags, and a GitHub-style issues view backed by in-repo Backlog.md tasks. It is read-only: it never writes git, so pushing and admin stay in your git server.

The look is the Bubble Tea / Charm terminal aesthetic on a CSS-token theme system — 7 themes (Flexoki / Uchu / Humdrum, each light + dark, plus e-ink).

It pairs naturally with Soft Serve (it can read Soft Serve's database to serve only public repos), but it works with any directory of bare *.git repos.

What you need

Required? For
Go 1.26+ to build compiling the single static binary
A dir of bare *.git repos yes the content custard serves
Soft Serve optional private/hidden gating + HTTPS clone proxy
Backlog.md tasks in a repo optional the per-repo Issues tab
A server + domain + Caddy to go public TLS + reverse proxy

Quick start (local)

go install github.com/a-h/templ/cmd/templ@latest   # one-time: the template generator
templ generate                                      # after editing any *.templ
go run ./cmd/custard --repos /path/to/bare/repos --addr :8080
# open http://localhost:8080

--repos is any directory containing name.git bare repositories. With no other flags, custard lists every repo it finds (fine for a fully public/local setup).

Fonts

The Charm look uses three commercial faces that are not bundled (licensing). Without them custard falls back to your system mono/sans — fully functional, just plainer. To get the intended type, drop your own .woff2 files in web/static/fonts/ matching the @font-face names in web/static/tokens.css (Awke, Untitled Sans, Name Mono). They're embedded into the binary at build time.

Configuration

All via flags or env vars (flag wins):

Flag Env Default Purpose
--repos REPOS_PATH ./repos directory of bare *.git repos
--addr LISTEN_ADDR :8080 listen address
--base-url BASE_URL http://localhost:8080 public base URL
--soft-serve-http SOFT_SERVE_HTTP http://localhost:23232 clone URL base shown in the footer
--soft-serve-db SOFT_SERVE_DB (empty) path to soft-serve.db; when set, only public (non-private, non-hidden) repos are served

Private repos: if you point custard at a Soft Serve repos dir, set --soft-serve-db so private/hidden repos are hidden from the list and 404 on direct access. Without it, every repo on disk is public — only do that if they're all meant to be public.

Deploy (self-host, public)

One script provisions Caddy (auto-TLS) + a systemd service, generating all config from your settings — nothing to hand-edit on the server.

# 1. DNS: point your domain's A record at the server.
# 2. Configure:
cp deploy/deploy.env.example deploy/deploy.env
$EDITOR deploy/deploy.env          # REMOTE, DOMAIN, RUN_USER, REPOS_PATH, ...
# 3. Deploy (re-run anytime to update):
deploy/deploy.sh

deploy.sh builds a static linux/amd64 binary, ships it to REMOTE, writes /etc/systemd/system/custard.service (custard runs as RUN_USER, bound to 127.0.0.1:8080, repos mounted read-only) and /etc/caddy/Caddyfile (TLS for DOMAIN; /git/* → your Soft Serve clone backend; /dl/* for release tarballs), then starts both. See deploy/deploy.env.example for every setting.

Not using Soft Serve? Leave SOFT_SERVE_DB empty (serves all repos) and SOFT_SERVE_BACKEND empty (drops the /git clone proxy). custard still serves any bare repos in REPOS_PATH.

How it fits together

            HTTPS                 read-only, on disk
browser ──▶ Caddy ──▶ custard ──▶ /path/to/repos/*.git   (go-git)
              │                └─▶ soft-serve.db          (visibility, optional)
              └─/git/*─▶ Soft Serve HTTP (clone, optional)

Releasing CLIs via a self-hosted Homebrew tap

The same server can host a Homebrew tap + release tarballs, so your command-line repos install with no GitHub. custard serves the tarballs (/dl) and the tap repo (/git); scripts/brew-release.sh does the rest.

# one-time: a `homebrew-tap` repo exists on the server (public)
# release a tagged repo (archives the tag from the server, publishes the tarball,
# writes a source-build formula, pushes the tap):
scripts/brew-release.sh <repo> <version> [go-package]
#   e.g.  scripts/brew-release.sh sportsball v0.1.0
#         scripts/brew-release.sh sportsball v0.1.0 ./cmd/sportsball   # if main isn't at root

Users then:

brew tap you/tap https://your-host/git/homebrew-tap.git
brew trust you/tap        # recent Homebrew gates third-party taps
brew install <repo>

Formulas build from source (depends_on "go") — no per-arch bottles, no GitHub.

See PLAN.md for the phased build plan.