Starter template for type-safe, server-rendered web applications on Go + Templ + HTMX + Tailwind + Alpine.js, wired up the way Effiware production apps are: Postgres for state, Redis for sessions and caches, OpenTelemetry for traces and metrics, and the app itself stateless.
- 📜 Type-safe templates — Templ, compile-time checked
- 🎨 HDA + JSON API — the same state served as hypermedia and as JSON
- 🐘 Postgres — pgx pool, goose migrations embedded and run at startup, sqlc-generated queries
- ⚡ Redis — session store and the cache wrappers in
utils/; optional - ⚙️ Config — viper: YAML file +
GOTH_TEMPLATE_*env overrides - 🔭 Observability — OTLP traces, Prometheus metrics on
/metrics, JSON logs carryingtrace_id - 🩺 Ops —
/pingliveness,/readyzreadiness, SIGTERM drain, graceful shutdown - 🔥 Hot reload — Air + the templ proxy
- 📘 Swagger — swag-generated docs at
/swagger
- Go v1.26, npm v11.4, node v24.4 (or just Docker 28.1)
- GNU Make (recommended)
Tools (templ, air, swag, sqlc, goose) are Go tool dependencies — make prep installs them.
make prep # tools, npm deps, .env and config.yaml from the examples
make air # infra containers + templ watcher + hot-reloading serverOpen http://localhost:8080. Click the button (HDA), then hit the same counter over JSON:
curl http://localhost:8080/api/v1/clicks
curl -X POST http://localhost:8080/api/v1/clicks/incrementmake air starts Postgres, Redis and Jaeger via docker-up-infra and tears them down on
exit. For the whole stack in containers instead: make docker-build && make docker-up.
cmd/server/ main.go (lifecycle) + bootstrap.go (boot* wiring — put boot logic here)
internal/
clicks/ demo domain package: Postgres state read through a Redis cache
config/ viper config, defaults and validation
db/ pgx pool, tx + advisory-lock helpers, migrations/, queries/, sqlc output
server/ HTTP: routes, middlewares/, hda/ (hypermedia), api/ (JSON), session/
services/ background jobs: Servicer + periodic/scheduled runners, gated fleet-wide
static/ css/, img/, js/ (htmx + alpine, vendored — no CDN)
views/ .templ pages and components/
version/ ldflags-injected version + the OTel instrumentation scope name
utils/ resilience primitives: retry, circuit, caches (see utils/README.md)
config.yaml (copy of config.example.yaml), with every key
overridable by env — GOTH_TEMPLATE_SERVER_PORT, GOTH_TEMPLATE_DATABASE_URL, … (dots
become underscores). .env is only for docker-compose port mapping and the goose CLI.
Redis is optional: with redis.url empty the session store and caches switch off and the
app keeps serving. Tracing is off when otlp.url is empty and no OTEL_* env is set.
| Target | Does |
|---|---|
prep |
install tools + npm deps, seed .env and config.yaml |
air |
infra up, templ watch, hot-reloading server, infra down on exit |
build / build-local |
Tailwind + Go build / Go only |
test / test-race |
tests with coverage (generated code excluded) / race detector |
templ-gen / sqlc / swag |
regenerate templates / queries / API docs |
db-migrate / db-migrate-down |
manual goose cycle (the app migrates itself at startup) |
docker-build / docker-up / docker-up-infra / docker-down |
containers |
- Rename the module (
go.mod),version.ServiceName, and the compose/image names. - Delete the demo:
internal/clicks,services/demo_service.go, migration003,queries/clicks.sql, the click routes, views andapi.Clicks. - Prune
internal/db/migrations/001–002down to the tables you actually want, thenmake sqlc. - Drop what you don't need — Redis, the session store,
services/,utils/— each is nil-safe or self-contained.
sudo lsof -i -P | grep LISTEN | grep :<PORT> # find the PID
sudo kill -9 <PID>Air rebuilds Tailwind + Go on change; templ generate --watch regenerates templates and
notifies its proxy, which reloads the browser. There is an in-depth explanation in
this
article.
go get -tool <package>@none