Scaffold¶
What burrow new actually generates, and how to work with it.
This page covers the output of the scaffold and the mise tasks it ships. The Tooling page documents the burrow CLI itself — installation, flag reference for burrow new, burrow generate app, and burrow tailwind. If you're not using the scaffold and want a hand-rolled layout, see Project Structure.
Generated layout¶
burrow new myapp --module github.com/you/myapp produces:
myapp/
├── cmd/
│ └── myapp/
│ └── main.go # NewServer + cli wiring; pins contrib stack
├── internal/
│ └── app/ # Shell app: layout, homepage, CSS bundle
│ ├── app.go # HasTemplates, HasStaticFiles, NavItems, Routes
│ ├── static/ # Tailwind output (app.min.css, gitignored placeholder .gitkeep)
│ └── templates/
│ ├── app/
│ │ ├── layout.html # Site layout with navbar, alerts, htmx
│ │ └── icons.html # Inline-SVG icon defines
│ ├── error/ # Tailwind-styled overrides for 403/404/500/…
│ │ └── errors.html
│ └── pages/
│ └── home.html
├── tailwind.css # Tailwind v4 entry — @source lines auto-generated by `burrow tailwind`
├── go.mod # Pinned to the burrow version that generated this project
├── .mise.toml # Tool versions + tasks (see table below)
├── .pre-commit-config.yaml # golangci-lint, govulncheck, go test, go mod tidy
├── .golangci.yml # Lint config
├── .goreleaser.yaml # Multi-arch binaries + distroless Docker image
├── .github/workflows/ci.yml # CI gate (test, lint, vuln, zizmor) + tag-only release job
├── Dockerfile # Distroless base; binaries pre-built by goreleaser
├── README.md # Project-level README with Quick Start, Development, Deployment
├── LICENSE # MIT, attributed to `--author`
└── .gitignore # Build output, .env, SQLite files, Tailwind output, CLAUDE.md, beans
The shell app under internal/app/ holds the layout, the homepage, the compiled CSS bundle, and the project-wide icon defines in a single project-owned package, so all embedded assets sit next to the code that uses them. main.go wires the layout via srv.SetLayout("app/layout"), where "app/layout" is the template name defined in internal/app/templates/app/layout.html. The rationale for the layout is in Tailwind CSS → Why the shell is in internal/app/; how layout template names resolve and wrap content is in Layouts & Rendering.
main.go wires the typical contrib stack out of the box: session, csrf, staticfiles, healthcheck, messages, htmx, plus the shell app. Add more contribs (auth, jobs, admin, …) by importing and listing them in burrow.NewServer(...).
Mise tasks¶
.mise.toml pins the Go toolchain, Tailwind v4, golangci-lint, tparse, goimports, govulncheck, goreleaser, and pre-commit. The tasks below are all defined in the same file; run mise tasks inside the generated project to see them with descriptions.
| Task | What it does |
|---|---|
mise run setup |
Bootstrap: mise install + go mod tidy + burrow dev --init-env (generates .env with persistent SESSION_HASH_KEY / CSRF_KEY) + pre-commit install (if available) |
mise run dev |
burrow dev — watches the project, rebuilds Tailwind CSS, restarts the Go app on every save, sequentially in one process |
mise run css |
One-off Tailwind build via burrow tailwind -i tailwind.css -o internal/app/static/app.min.css --minify. burrow tailwind wraps the standalone Tailwind v4 Rust CLI — no node_modules, no npx. |
mise run test |
go test ./... piped through tparse |
mise run lint |
golangci-lint run ./... |
mise run fmt |
gofmt -w . + goimports -w . |
mise run vuln |
govulncheck ./... |
mise run coverage |
go test -coverprofile=coverage.out ./... → coverage.html |
mise run tidy |
go mod tidy |
mise run clean |
Remove build/, dist/, .tailwind/, coverage.*, the compiled CSS bundle, *.coverprofile |
mise run clean-all |
clean plus data/ and every *.db* SQLite file in the tree (keeps .env) |
mise run install |
go install a stripped, versioned binary into $GOPATH/bin |
mise run release-snapshot |
goreleaser release --snapshot --clean --skip=publish,docker — local sanity check, no publish |
mise run release |
goreleaser release --clean — full release: archives for every OS/arch + multi-arch Docker image (needs v* tag + GH/ghcr auth, normally only the CI release job runs this) |
mise run release-no-docker |
Like release but skips the Docker image step |
The dev loop¶
mise run setuponce afterburrow new(or after cloning a teammate's project).mise run dev. The first run also writes.envif it doesn't exist, with mode0600. The file is gitignored —burrow devreads it as a dotenv and injects every key into the app's environment before launching.- Edit anything under
internal/app/templates/, any*.gofile, or any*.toml/*.yml/*.yaml(translation bundles, app config).burrow devdebounces, rebuilds Tailwind CSS when templates change, then restarts the Go binary. Server defaults to http://localhost:8080.
The pinned SESSION_HASH_KEY and CSRF_KEY in .env survive restarts so logged-in sessions and CSRF tokens don't break every save.
The data/ directory is created automatically on first boot — Den's SQLite backend runs os.MkdirAll on the parent directory of the DSN path (default sqlite:///data/app.db), so you don't have to seed an empty data/ to make the app start.
Adding apps¶
Inside the generated project:
Produces internal/notes/{app.go, app_test.go, templates/notes/index.html}. The stub registers GET /notes and renders notes/index. The next-steps hint prints the full import path derived from the host module's go.mod — copy it into cmd/myapp/main.go's burrow.NewServer(...) call to wire the app in.
See Tooling for the flags and naming rules.
Release flow¶
The scaffold ships a goreleaser-driven release pipeline:
- Local sanity check:
mise run release-snapshotbuilds for every supported OS/arch intodist/without publishing or building Docker images. Useful for catching.goreleaser.yamlmistakes before tagging. - Cut a release: push a
v*tag tomain. The CI workflow (.github/workflows/ci.yml) runs the standardcheckandzizmorjobs first; on success the tag-onlyreleasejob runsmise run release, which produces multi-arch binaries plus a distroless Docker image pushed toghcr.io/<git-user>/<project>. - Local fallback:
mise run release-no-dockerexists for the case where ghcr auth isn't available — binaries-only release.
The Dockerfile uses gcr.io/distroless/static-debian12:nonroot. SQLite lives at /data/app.db by default; mount a volume there (writable by UID 65532) to persist data across container restarts. The Dockerfile itself documents the run command.
The framework-level release decisions (build flags, version pinning, archive layout) are covered in Releases.
Pre-commit hooks¶
mise run setup installs the hooks from .pre-commit-config.yaml. Before every commit they run:
golangci-lint run ./...govulncheck ./...go test ./...go mod tidy(only whengo.modorgo.sumchanged)
All four call the mise-installed tools directly, so no extra Go toolchain or Python virtualenv is needed beyond what mise run setup already put in place. The hooks make the local commit gate match the CI gate.
What the scaffold deliberately omits¶
- No CSS framework contrib. Burrow is CSS-agnostic at the framework level; the scaffold ships a Tailwind v4 pipeline as the recommended setup, but nothing forces you to keep it. Swap
tailwind.cssand theinternal/app/templates/app/layout.htmlfor any other stack. - No auth/admin out of the box.
contrib/authandcontrib/adminare not registered in the generatedmain.go— opt in by importing and listing them inburrow.NewServer(...)when you need them. - No PostgreSQL config. The DSN defaults to
sqlite:///data/app.db. Switch to PostgreSQL by setting--database-dsn postgres://…(orDATABASE_DSN=…) and blank-importinggithub.com/oliverandrich/den/backend/postgresnext to the SQLite import inmain.go.