# 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](https://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](https://charm.sh) 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](https://github.com/charmbracelet/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](https://github.com/charmbracelet/soft-serve) | optional | private/hidden gating + HTTPS clone proxy | | [Backlog.md](https://backlog.md) tasks in a repo | optional | the per-repo Issues tab | | A server + domain + [Caddy](https://caddyserver.com) | to go public | TLS + reverse proxy | ## Quick start (local) ```bash 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. ```bash # 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](https://brew.sh) 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. ```bash # 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 [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: ```bash brew tap you/tap https://your-host/git/homebrew-tap.git brew trust you/tap # recent Homebrew gates third-party taps brew install ``` Formulas build from source (`depends_on "go"`) — no per-arch bottles, no GitHub. ### Auto-release on tag (webhook) custard can publish automatically when you push a tag — no manual script. Opt a repo in with a `.custard.yaml` at its root: ```yaml brew: enabled: true package: . # go build path; default "." ``` Configure custard with a `WEBHOOK_SECRET` (set it in `deploy.env`), then register a Soft Serve webhook on the repo: ```bash ssh soft repo webhook create https://your-host/hooks/release \ -e branch_tag_create -c json -s "$WEBHOOK_SECRET" ``` Now releasing is just: ```bash git tag -a v0.2.0 -m "..." && git push origin v0.2.0 ``` The tag (must be semver `vX.Y.Z`) fires the webhook → custard verifies the HMAC signature, archives the tag, writes the tarball, and pushes an updated formula to the tap. `scripts/brew-release.sh` remains as a manual fallback. See `PLAN.md` for the phased build plan.