Skip to content

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

  1. mise run setup once after burrow new (or after cloning a teammate's project).
  2. mise run dev. The first run also writes .env if it doesn't exist, with mode 0600. The file is gitignored — burrow dev reads it as a dotenv and injects every key into the app's environment before launching.
  3. Edit anything under internal/app/templates/, any *.go file, or any *.toml / *.yml / *.yaml (translation bundles, app config). burrow dev debounces, 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:

burrow generate app notes

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:

  1. Local sanity check: mise run release-snapshot builds for every supported OS/arch into dist/ without publishing or building Docker images. Useful for catching .goreleaser.yaml mistakes before tagging.
  2. Cut a release: push a v* tag to main. The CI workflow (.github/workflows/ci.yml) runs the standard check and zizmor jobs first; on success the tag-only release job runs mise run release, which produces multi-arch binaries plus a distroless Docker image pushed to ghcr.io/<git-user>/<project>.
  3. Local fallback: mise run release-no-docker exists 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 when go.mod or go.sum changed)

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.css and the internal/app/templates/app/layout.html for any other stack.
  • No auth/admin out of the box. contrib/auth and contrib/admin are not registered in the generated main.go — opt in by importing and listing them in burrow.NewServer(...) when you need them.
  • No PostgreSQL config. The DSN defaults to sqlite:///data/app.db. Switch to PostgreSQL by setting --database-dsn postgres://… (or DATABASE_DSN=…) and blank-importing github.com/oliverandrich/den/backend/postgres next to the SQLite import in main.go.