# 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/v5` reads bare repos directly off disk — no shelling to `git`. `templ` for typed templates, `chroma` for syntax highlight, `goldmark` for markdown, `gopkg.in/yaml.v3` for backlog frontmatter. Router = stdlib `net/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/.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 via `data-theme` on `` + cookie (app-kit pattern), default `flexoki`. - **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 home: README render + branch/tag/commit summary | | `/r//tree//` | directory browse | | `/r//blob//` | file view — chroma highlight, or goldmark for `.md` | | `/r//raw//` | raw bytes | | `/r//log/` | commit log (paged) | | `/r//commit/` | commit diff | | `/r//refs` | branches + tags | | `/r//issues` | **backlog tasks, GitHub-issues style** — filter by status/label | | `/r//issues/` | single task: rendered body + acceptance criteria | | `/dl//.tar.gz` | release tarball *(phase 4)* | | `/git/.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/backlog` reads `backlog/tasks/`, splits frontmatter (`id, title, status, labels, assignee, dependencies, created_date, updated_date, ordinal`) from the md body, parses with `yaml.v3`, renders body via goldmark. - Issues list: group/filter by `status` (To Do / In Progress / Done), color chips from `labels` (feature/bug/…) mapped onto theme color tokens. Sort by `ordinal`. - 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 `backlog` CLI + 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 1. **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/*.git` for dev. 2. **Issues view** — `internal/backlog` parser + `/issues` list and detail. 3. **Styling pass** — port `tokens.css` + fonts, build `custard.css` (terminal/Charm chrome), theme switcher (7 modes, cookie, no-flash SSR), polish all views. 4. **Homebrew tap + release pipeline** — `git archive` tag → `name-X.Y.Z.tar.gz` in `/dl` + sha256; `homebrew-tap` repo in Soft Serve; GoReleaser emits formula, ~10-line script bumps `url`/`sha256`/`version` and pushes the tap. Source-build formula (no GitHub, no bottles). 5. **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) ```bash 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 so `brew install` never breaks.