custard — build plan
A custom, public-facing web code forge over my self-hosted Soft Serve git server. Own design — not a skinned cgit/gitweb. Reads bare repos directly; renders my own templates. Aesthetic = Bubble Tea / Charm terminal vibe + _shared-app-kit tokens (Flexoki / Uchu / Humdrum themes, Awke / Untitled Sans / Name Mono fonts).
Soft Serve is the git host (SSH push/TUI, read-only HTTP clone, webhooks). custard is a separate read-only Go web app pointed at the same bare repos on disk. It never writes git.
Decisions (locked)
- Stack: pure Go (1.26).
go-git/v5reads bare repos directly off disk — no shelling togit.templfor typed templates,chromafor syntax highlight,goldmarkfor markdown,gopkg.in/yaml.v3for backlog frontmatter. Router = stdlibnet/http(Go 1.22+ method+pattern mux) — no web framework. - Render: dynamic server. Each request reads the bare repo live → template → HTML. Always fresh; leaves room for private-repo auth later. Caddy in front for auto-TLS.
- Hosting: all-on-droplet.
Caddy → custard(reads./repos/*.git); Caddy reverse-proxies/git/<repo>.git/*→ Soft Serve HTTP (:23232) for clone, and serves/dl/*tarballs. - Tokens are plain CSS custom properties → port
tokens.css+ fonts straight in; no Tailwind/Next needed. Theme viadata-themeon<html>+ cookie (app-kit pattern), defaultflexoki. - jj: Soft Serve is git-only; jj repos colocate a
.git, so go-git reads them unchanged. Optionally show a "jj" badge when.jj/is present. No special handling required.
Layout
custard/
├─ cmd/custard/main.go flags/env, http.Server, graceful shutdown
├─ internal/
│ ├─ config/ REPOS_PATH, SOFT_SERVE_HTTP, LISTEN_ADDR, BASE_URL
│ ├─ gitread/ go-git wrappers: list, refs, tree, blob, log, commit, archive
│ ├─ backlog/ parse backlog/tasks/*.md → Task{frontmatter + md body}
│ ├─ render/ chroma highlight, goldmark md, byte/lang detection
│ └─ server/ handlers, routes, middleware, error pages
├─ web/
│ ├─ templates/*.templ layout, list, repo, tree, blob, log, commit, refs, issues, issue
│ └─ static/
│ ├─ tokens.css ported from _shared-app-kit (Next bits stripped)
│ ├─ custard.css app chrome — terminal/Charm look on the token system
│ ├─ theme.js data-theme cookie toggle (7 modes)
│ └─ fonts/ Awke, Untitled Sans, Name Mono (.woff2)
├─ Caddyfile
├─ deploy/ systemd unit + release.sh
├─ go.mod PLAN.md CLAUDE.md
URL scheme (cgit-inspired)
| Route | View |
|---|---|
/ |
repo list (public, non-hidden) — name, description, last commit |
/r/<repo> |
repo home: README render + branch/tag/commit summary |
/r/<repo>/tree/<ref>/<path> |
directory browse |
/r/<repo>/blob/<ref>/<path> |
file view — chroma highlight, or goldmark for .md |
/r/<repo>/raw/<ref>/<path> |
raw bytes |
/r/<repo>/log/<ref> |
commit log (paged) |
/r/<repo>/commit/<sha> |
commit diff |
/r/<repo>/refs |
branches + tags |
/r/<repo>/issues |
backlog tasks, GitHub-issues style — filter by status/label |
/r/<repo>/issues/<id> |
single task: rendered body + acceptance criteria |
/dl/<repo>/<file>.tar.gz |
release tarball (phase 4) |
/git/<repo>.git/* |
clone — Caddy reverse-proxy → Soft Serve HTTP (deploy) |
Backlog → issues view (the GitHub-issues surface)
Issues already travel in-repo at backlog/tasks/*.md (YAML frontmatter + Markdown body), so
no separate data source — go-git reads them from the chosen ref like any other file.
internal/backlogreadsbacklog/tasks/, splits frontmatter (id, title, status, labels, assignee, dependencies, created_date, updated_date, ordinal) from the md body, parses withyaml.v3, renders body via goldmark.- Issues list: group/filter by
status(To Do / In Progress / Done), color chips fromlabels(feature/bug/…) mapped onto theme color tokens. Sort byordinal. - Issue detail: title, status badge, labels, dates, dependencies, then rendered body (description + acceptance criteria checklist).
- Read-only (a public forge view). No create/edit — that stays in the
backlogCLI + git push. - Repos with no
backlog/tasks/dir simply hide the Issues tab. - Nice loop: custard's own backlog renders inside custard once deployed.
Phases
- Read layer + core views — go-git wrappers + handlers for list, repo, tree, blob, log,
commit, refs. Unstyled HTML. Point at a local clone of
./repos/*.gitfor dev. - Issues view —
internal/backlogparser +/issueslist and detail. - Styling pass — port
tokens.css+ fonts, buildcustard.css(terminal/Charm chrome), theme switcher (7 modes, cookie, no-flash SSR), polish all views. - Homebrew tap + release pipeline —
git archivetag →name-X.Y.Z.tar.gzin/dl+ sha256;homebrew-taprepo in Soft Serve; GoReleaser emits formula, ~10-line script bumpsurl/sha256/versionand pushes the tap. Source-build formula (no GitHub, no bottles). - Deploy — Caddyfile (auto-TLS), systemd unit, reverse-proxy
/git/*→ Soft Serve:23232, serve/dl. Run on droplet next to Soft Serve's data dir.
Dev commands (target)
go install github.com/a-h/templ/cmd/templ@latest # one-time: templ generator
templ generate # .templ → _templ.go
go run ./cmd/custard --repos ./repos --addr :8080 # local dev
go build -o custard ./cmd/custard # release binary
Open / later
- Private repos: dynamic server can gate later (read Soft Serve auth model); public-only first.
- Search across blobs/commits — defer.
- Atom feeds per repo (cgit has them) — cheap add later.
- Self-hosting = I own uptime/TLS; keep
/dl+ Caddy reliable sobrew installnever breaks.