diff --git a/.planning/phases/01-foundation/01-RESEARCH.md b/.planning/phases/01-foundation/01-RESEARCH.md new file mode 100644 index 0000000..6a4b16c --- /dev/null +++ b/.planning/phases/01-foundation/01-RESEARCH.md @@ -0,0 +1,799 @@ +# Phase 1: Foundation - Research + +**Researched:** 2026-05-14 +**Domain:** Go web server scaffold (chi + templ + HTMX + Tailwind + pgx/pgxpool + goose + sqlc) +**Confidence:** HIGH + + +## User Constraints (from CONTEXT.md) + +### Locked Decisions + +**Directory Layout** +- **D-01:** Two-binary layout with shared `internal/`: + ``` + backend/ + cmd/web/main.go + cmd/worker/main.go + internal/ + db/ (sqlc-generated queries + pgx pool wiring) + web/ (chi router, handlers, middleware) + session/ (placeholder package — populated in Phase 2) + tablos/ (placeholder — Phase 3) + tasks/ (placeholder — Phase 4) + files/ (placeholder — Phase 5) + migrations/ (goose .sql files) + templates/ (.templ files) + static/ (tailwind.css output, htmx.min.js) + compose.yaml + justfile + .env.example + README.md + ``` +- **D-02:** Phase 1 creates the directory skeleton for all `internal/` packages (empty `doc.go` is fine) so later phases drop files in without restructuring. +- **D-03:** `cmd/worker` in Phase 1 is a minimal binary that boots, connects to Postgres, logs "worker ready", and exits cleanly on signal. Real job runtime is Phase 6. + +**Migrations** +- **D-04:** Use **goose** (`pressly/goose`). Embeddable library + CLI; supports Go-based migrations; one `.sql` per migration with `-- +goose Up/Down` annotations. +- **D-05:** `just migrate up` / `just migrate down` / `just migrate status` wired via the goose CLI for local dev. Production migration strategy (embed vs CLI) decided in Phase 7. +- **D-06:** Phase 1 includes one trivial bootstrap migration (e.g., `0001_init.sql`) so the migration pipeline is exercised end-to-end. + +**Templating + Router** +- **D-07:** **templ** (`a-h/templ`) for HTML. +- **D-08:** **chi** (`go-chi/chi/v5`) as the HTTP router. Middleware stack: `RequestID → RealIP → Logger (structured) → Recoverer → GracefulShutdown wiring`. +- **D-09:** `templ generate` runs via `just generate` (alongside `sqlc generate`). +- **D-10:** Base layout template renders a Tailwind-styled page with HTMX loaded from `/static/htmx.min.js` (vendored, not CDN). Include one working `hx-get` example. + +**Local Dev Stack** +- **D-11:** **podman compose** for local Postgres (`backend/compose.yaml`). README documents that docker compose also works. +- **D-12:** **Standalone Tailwind CLI binary** (no Node/pnpm in `backend/`). Downloaded by a `just bootstrap` recipe into `./bin/tailwindcss` (gitignored); version pinned in the justfile. +- **D-13:** **air** (`cosmtrek/air` → now `air-verse/air`) for Go live-reload (`just dev`). Watches `.go` + `.templ`; triggers `templ generate` and rebuild. +- **D-14:** Tailwind in watch mode runs as a separate process (`just styles`) or via air's `pre_cmd` — planner decides. + +**Configuration & Operational Basics** +- **D-15:** Env-driven config via `.env`. Required keys: `DATABASE_URL`, `PORT`, `ENV`. Provide `.env.example`. +- **D-16:** Postgres driver: **pgx/v5** with `pgxpool`. sqlc emits pgx-compatible code (`sqlc.yaml` engine: postgresql, sql_package: pgx/v5). +- **D-17:** Structured logging: `log/slog` (Go 1.21+) with JSON handler in prod, text in dev, switched by `ENV`. +- **D-18:** Request ID middleware attaches a UUID per request and threads it into slog via `context.Context`. +- **D-19:** Graceful shutdown: `cmd/web` traps SIGINT/SIGTERM, calls `http.Server.Shutdown` (default 10s), then closes the pgx pool. +- **D-20:** `/healthz` returns 200 with `{"status":"ok","db":"ok"}` only when `db.Ping` succeeds; otherwise 503 with `{"status":"degraded","db":"down"}`. + +### Claude's Discretion +- Concrete chi middleware order within the agreed stack and slog handler configuration details. +- Exact `air.toml` settings, file watch globs, and whether tailwind runs as a separate `just styles` process or air `pre_cmd`. +- Whether goose runs migrations via library call from a `backend migrate` subcommand or pure CLI in Phase 1. +- Layout/CSS specifics of the demo page (minimal but professional; one `hx-get` interaction is enough). +- Whether to include a basic `internal/web/handlers_test.go` smoke test now or defer to Phase 2. + +### Deferred Ideas (OUT OF SCOPE) +- Single-binary subcommand layout (`backend web` / `backend worker`). +- Embedded goose migrations called from app startup (Phase 1 uses CLI-only). +- `/readyz` endpoint (DEPLOY-04, Phase 7). +- Production logging configuration (sampling, redaction, log shipping). +- Full handler test suite (Phase 2 establishes testing strategy). + + + +## Phase Requirements + +| ID | Description | Research Support | +|----|-------------|------------------| +| FOUND-01 | Fresh `backend/` Go package with module init, `cmd/web` and `cmd/worker` entrypoints, runnable HTTP server returning `/healthz` | Standard Stack (Go, chi, pgx); Architecture (two-binary layout, healthz handler pattern) | +| FOUND-02 | Postgres connection pool with env-driven config and a versioned migration tool wired into a `justfile` | Standard Stack (pgxpool, goose); Code Examples (pgxpool init, goose CLI invocation) | +| FOUND-03 | HTMX + Tailwind + templ rendering pipeline producing a base layout with a working dev loop (template hot-reload, CSS rebuild) | Standard Stack (templ, tailwind standalone, air); Architecture (static asset serving, dev loop) | +| FOUND-04 | Structured logging, request ID middleware, and graceful shutdown on the web server | Standard Stack (log/slog, chi middleware); Code Examples (slog handler switch, RequestID propagation, http.Server.Shutdown) | +| FOUND-05 | `.env.example`, local Postgres via `compose.yaml`, and a `justfile` documenting `dev`, `migrate`, `test`, `lint` | Architecture (compose.yaml shape, justfile recipes); Environment Availability (podman, just, Go, tailwind binary) | + + +## Project Constraints (from CLAUDE.md) + +The repo CLAUDE.md describes the existing JS monorepo. The Go rewrite section explicitly establishes: +- Go + HTMX + Tailwind + Postgres + sqlc — **no third-party auth, no JS framework, no managed BaaS** in `backend/`. +- Server-managed sessions only (HTTP-only cookies). No JWTs. +- One web binary + one worker binary, same repo. +- Single VPS / container deploy. No Kubernetes. +- GSD workflow enforcement: use `/gsd-execute-phase` for phase work — direct edits outside GSD are disallowed. + +These directly constrain Phase 1: no Node/npm dependency inside `backend/`, no JWT libraries, no Auth provider SDKs. The Tailwind standalone CLI choice exists specifically to honor "no JS toolchain in `backend/`." + +## Summary + +Phase 1 is a Walking Skeleton: the thinnest end-to-end slice that proves `air → templ → chi → pgxpool → Postgres → goose → tailwind` all wire together and live-reload on a developer's machine. Every architectural decision has already been locked in CONTEXT.md, so research focuses on **verified versions, canonical wiring patterns, and known pitfalls** rather than alternatives. + +The stack is well-trodden — the developer's pre-existing `go-backend/` scratch directory already demonstrates a working templ + chi + pgx + sqlc + podman compose pipeline. The new `backend/` discards the pnpm-Tailwind path in favor of the standalone Tailwind binary and adds goose for migrations (which `go-backend/` did not use). + +**Primary recommendation:** Build the smallest possible end-to-end loop first (web boots → `/healthz` calls `db.Ping` → root route renders one templ page with one `hx-get` button → goose applies one no-op migration), then layer in slog/RequestID/graceful shutdown. Resist adding anything that does not satisfy a FOUND-XX requirement. + +## Architectural Responsibility Map + +| Capability | Primary Tier | Secondary Tier | Rationale | +|------------|-------------|----------------|-----------| +| HTTP routing & middleware | Go server (`internal/web`) | — | chi router owns all request lifecycle | +| HTML rendering | Go server (templ → HTML) | Browser (HTMX swaps) | templ renders server-side; HTMX issues partial-fetch round-trips | +| Partial fragment fetch | Browser (HTMX `hx-get`) | Go server (templ partial) | HTMX makes the request; server returns an HTML fragment | +| DB connection pool | Go server (`internal/db`, pgxpool) | — | Single pool wired at startup, shared across handlers | +| Migrations | CLI (goose) against local Postgres | — | Phase 1 is CLI-driven via justfile; library embedding deferred | +| Static asset delivery | Go server (`http.FileServer` from `/static`) | — | Self-hosted (no CDN); `htmx.min.js` + `tailwind.css` vendored | +| CSS build | Local toolchain (Tailwind standalone CLI) | — | Compile-time artifact in `static/tailwind.css` | +| Live reload | Local toolchain (air) | — | Dev-only; watches `.go` + `.templ` | +| Process supervision | OS (signal handling in `cmd/web` and `cmd/worker`) | — | SIGINT/SIGTERM → graceful shutdown | +| Containerized Postgres | Local container runtime (podman compose) | — | Local dev only; prod Postgres is external (Phase 7) | +| Observability (logs) | Go server (`log/slog` to stdout) | — | JSON in prod, text in dev. No shipping in Phase 1. | + +## Standard Stack + +### Core +| Library | Version | Purpose | Why Standard | +|---------|---------|---------|--------------| +| Go | 1.22+ (existing `go-backend/` uses 1.26) | Runtime | chi v5.2+ requires Go 1.22 minimum [VERIFIED: chi v5.2.5 release notes] | +| `github.com/go-chi/chi/v5` | v5.2.5 | HTTP router + middleware | Idiomatic, minimal, standard middleware set [VERIFIED: GitHub releases, Feb 5] | +| `github.com/a-h/templ` | v0.3.1020 | Type-safe HTML templates | Compiled, type-checked at build time; first-class HTMX fit [VERIFIED: GitHub releases, May 10] | +| `github.com/jackc/pgx/v5` | v5.9.2 | Postgres driver + pgxpool | Higher performance and richer types than `database/sql`; sqlc's recommended driver [VERIFIED: tags page, Apr 19, 2026] | +| `github.com/pressly/goose/v3` | v3.27.1 | DB migrations (CLI + library) | Embeddable, single-file SQL migrations, supports Go migrations [VERIFIED: GitHub releases, Apr 24] | +| `github.com/sqlc-dev/sqlc` | v1.31.1 | SQL → typed Go code generator | Type-safe queries, pgx integration [VERIFIED: GitHub releases, Apr 22] | +| `log/slog` | std lib (Go 1.21+) | Structured logging | Standard library; no external dep [CITED: pkg.go.dev/log/slog] | + +### Supporting +| Library | Version | Purpose | When to Use | +|---------|---------|---------|-------------| +| `github.com/google/uuid` | v1.6.0 | Request ID generation | RequestID middleware emits UUIDv4 per request [VERIFIED: go.mod in existing go-backend] | +| `github.com/air-verse/air` | v1.65.1 (CLI; not imported) | Go live-reload | Dev-only; configured via `.air.toml` [VERIFIED: GitHub releases, Apr 12; repo moved from `cosmtrek/air` to `air-verse/air`] | +| Tailwind standalone CLI | v4.x (pin in justfile) | CSS compile | Avoids Node/pnpm in `backend/` [CITED: tailwindcss.com/blog/standalone-cli] | +| HTMX | v2.x (vendor `htmx.min.js` into `static/`) | Client-side AJAX | Required for `hx-get` demo (success criterion 3) [ASSUMED: latest stable; planner verifies during execution] | +| `just` | latest | Task runner | Already in use in `go-backend/`; project standard [VERIFIED: existing justfile] | +| `podman compose` | matches developer's machine | Local Postgres | Locked in D-11 [VERIFIED: existing go-backend uses podman] | + +### Alternatives Considered (rejected per CONTEXT.md) +| Instead of | Could Use | Tradeoff | +|------------|-----------|----------| +| chi | net/http 1.22 ServeMux | Smaller dep budget but hand-rolled middleware composition; rejected | +| goose | golang-migrate, atlas | golang-migrate is split-file; atlas is declarative/heavier — rejected for embeddability + sqlc alignment | +| templ | html/template | Templ is type-checked at compile time; html/template is runtime-typed — rejected | +| Tailwind standalone | pnpm + tailwindcss npm | Would reintroduce Node toolchain — rejected (load-bearing decision) | +| podman | docker | Developer machine standard — both supported via portable `compose.yaml` | + +**Installation:** +```bash +# Go module +go mod init backend +go get github.com/go-chi/chi/v5@v5.2.5 +go get github.com/a-h/templ@v0.3.1020 +go get github.com/jackc/pgx/v5@v5.9.2 +go get github.com/pressly/goose/v3@v3.27.1 +go get github.com/google/uuid@v1.6.0 + +# CLI tools (developer machine) +go install github.com/pressly/goose/v3/cmd/goose@v3.27.1 +go install github.com/a-h/templ/cmd/templ@v0.3.1020 +go install github.com/sqlc-dev/sqlc/cmd/sqlc@v1.31.1 +go install github.com/air-verse/air@v1.65.1 + +# Tailwind standalone (just bootstrap recipe) +curl -sLO https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-{os}-{arch} +chmod +x tailwindcss-{os}-{arch} +mv tailwindcss-{os}-{arch} backend/bin/tailwindcss +``` + +**Version verification:** All versions listed above were checked against GitHub releases on 2026-05-14. Re-verify with `go list -m -u ` before pinning if more than ~30 days elapse before Phase 1 lands. + +## Architecture Patterns + +### System Architecture Diagram + +``` +┌───────────────────────────────────────────────────────────────┐ +│ Developer Machine │ +│ │ +│ just dev │ +│ ├─▶ podman compose up -d postgres ──▶ Postgres :5432 │ +│ ├─▶ tailwind --watch ──▶ static/tailwind.css │ +│ └─▶ air ──▶ rebuilds cmd/web on .go/.templ change │ +│ │ +│ just migrate up ──▶ goose CLI ──▶ migrations/*.sql ──▶ DB │ +└───────────────────────────────────────────────────────────────┘ + │ + ▼ +┌───────────────────────────────────────────────────────────────┐ +│ cmd/web │ +│ │ +│ main() ─▶ load env (.env) │ +│ ─▶ slog handler (JSON/text by ENV) │ +│ ─▶ pgxpool.New(DATABASE_URL) │ +│ ─▶ chi.NewRouter() │ +│ ├─ RequestID (uuid → ctx → slog) │ +│ ├─ RealIP │ +│ ├─ Logger (slog-backed) │ +│ ├─ Recoverer │ +│ ├─ /healthz ──▶ db.Ping → JSON │ +│ ├─ /static/* ──▶ http.FileServer(static/) │ +│ ├─ / ──▶ templ Layout(Index) │ +│ └─ /demo/time ──▶ templ Fragment (hx-get target) │ +│ ─▶ http.Server.ListenAndServe │ +│ ─▶ SIGINT/SIGTERM → Server.Shutdown(10s) → pool.Close │ +└───────────────────────────────────────────────────────────────┘ + │ + ▼ +┌───────────────────────────────────────────────────────────────┐ +│ Browser │ +│ GET / ──▶ HTML (layout + htmx.min.js + Tailwind CSS) │ +│ Button click ──▶ hx-get /demo/time ──▶ HTML fragment swap │ +└───────────────────────────────────────────────────────────────┘ + +┌───────────────────────────────────────────────────────────────┐ +│ cmd/worker (Phase 1: skeleton only) │ +│ main() ─▶ pgxpool.New ─▶ slog "worker ready" ─▶ wait on sig │ +└───────────────────────────────────────────────────────────────┘ +``` + +### Recommended Project Structure +``` +backend/ +├── cmd/ +│ ├── web/main.go # web entrypoint (chi server) +│ └── worker/main.go # worker entrypoint (skeleton) +├── internal/ +│ ├── db/ +│ │ ├── doc.go # package doc +│ │ ├── pool.go # pgxpool.New wrapper +│ │ └── sqlc/ # generated (empty in Phase 1) +│ ├── web/ +│ │ ├── router.go # chi.Router assembly +│ │ ├── handlers.go # /healthz, /, /demo/time +│ │ ├── middleware.go # RequestID + slog +│ │ └── handlers_test.go # (optional, Claude's discretion) +│ ├── session/doc.go # placeholder (Phase 2) +│ ├── tablos/doc.go # placeholder (Phase 3) +│ ├── tasks/doc.go # placeholder (Phase 4) +│ └── files/doc.go # placeholder (Phase 5) +├── templates/ +│ ├── layout.templ # base HTML + +│ ├── index.templ # root page with hx-get button +│ └── fragments.templ # server-rendered partials +├── migrations/ +│ └── 0001_init.sql # no-op or schema_migrations baseline +├── static/ +│ ├── htmx.min.js # vendored +│ └── tailwind.css # generated by tailwind standalone +├── bin/ # gitignored — tailwind CLI lives here +├── .air.toml +├── .env.example # DATABASE_URL, PORT, ENV +├── .gitignore # bin/, tailwind.css, tmp/, .env +├── compose.yaml # Postgres service +├── go.mod / go.sum +├── justfile # dev, migrate, generate, test, lint, build +├── sqlc.yaml # engine: postgresql, sql_package: pgx/v5 +├── tailwind.input.css # @tailwind base/components/utilities +└── README.md # 5-minute quickstart +``` + +### Pattern 1: pgxpool wiring with health check +**What:** Create a single `*pgxpool.Pool` at startup, share across handlers via a struct, expose `Ping` for `/healthz`. +**When to use:** Every Go+Postgres service; required for FOUND-02 and the `/healthz` DB check. +**Example:** +```go +// Source: pkg.go.dev/github.com/jackc/pgx/v5/pgxpool (canonical pattern) +import "github.com/jackc/pgx/v5/pgxpool" + +func NewPool(ctx context.Context, dsn string) (*pgxpool.Pool, error) { + cfg, err := pgxpool.ParseConfig(dsn) + if err != nil { + return nil, err + } + cfg.MaxConns = 10 + cfg.MinConns = 1 + return pgxpool.NewWithConfig(ctx, cfg) +} + +// In /healthz handler: +if err := pool.Ping(r.Context()); err != nil { + w.WriteHeader(http.StatusServiceUnavailable) + json.NewEncoder(w).Encode(map[string]string{"status":"degraded","db":"down"}) + return +} +w.WriteHeader(http.StatusOK) +json.NewEncoder(w).Encode(map[string]string{"status":"ok","db":"ok"}) +``` + +### Pattern 2: chi middleware order (standard) +**What:** Stack middleware so request IDs/IPs are available to the logger, panics never escape, and shutdown signals are honored. +**When to use:** Every chi router in this project. +**Example:** +```go +// Source: github.com/go-chi/chi v5 README (canonical middleware order) +r := chi.NewRouter() +r.Use(middleware.RequestID) // chi-provided; or custom UUID-emitting middleware +r.Use(middleware.RealIP) +r.Use(slogLoggingMiddleware) // custom: reads RequestID from ctx, attaches to slog.Logger +r.Use(middleware.Recoverer) // recovers from panics; must be after Logger +``` + +### Pattern 3: slog handler switch by ENV +**What:** Text handler in dev (human-readable), JSON handler in prod (machine-parseable). +**When to use:** App startup in both `cmd/web` and `cmd/worker`. +**Example:** +```go +// Source: pkg.go.dev/log/slog (handler constructors) +var handler slog.Handler +opts := &slog.HandlerOptions{Level: slog.LevelInfo} +if os.Getenv("ENV") == "production" { + handler = slog.NewJSONHandler(os.Stdout, opts) +} else { + handler = slog.NewTextHandler(os.Stdout, opts) +} +slog.SetDefault(slog.New(handler)) +``` + +### Pattern 4: RequestID → context → slog +**What:** Generate UUID per request, attach to `context.Context`, derive a per-request `*slog.Logger`. +**When to use:** All HTTP handlers — required for FOUND-04. +**Example:** +```go +type ctxKey string +const requestIDKey ctxKey = "request_id" + +func RequestID(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + id := uuid.NewString() + ctx := context.WithValue(r.Context(), requestIDKey, id) + w.Header().Set("X-Request-ID", id) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func LoggerFromContext(ctx context.Context) *slog.Logger { + if id, ok := ctx.Value(requestIDKey).(string); ok { + return slog.Default().With("request_id", id) + } + return slog.Default() +} +``` + +### Pattern 5: Graceful shutdown +**What:** Trap SIGINT/SIGTERM, call `http.Server.Shutdown`, close pgxpool, exit. +**When to use:** Both `cmd/web` and `cmd/worker`. +**Example:** +```go +// Source: chi v5 graceful shutdown example +srv := &http.Server{Addr: ":"+port, Handler: router} +go func() { + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + slog.Error("server error", "err", err); os.Exit(1) + } +}() +sig := make(chan os.Signal, 1) +signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) +<-sig +ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) +defer cancel() +if err := srv.Shutdown(ctx); err != nil { slog.Error("shutdown", "err", err) } +pool.Close() +``` + +### Pattern 6: templ + chi handler integration +**What:** templ components implement `Render(ctx, io.Writer) error`. Write directly from a chi handler. +**Example:** +```go +// Source: templ.guide (HTTP server integration) +func indexHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + _ = templates.Index().Render(r.Context(), w) +} + +// For HTMX fragment: +func demoTimeHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + _ = templates.TimeFragment(time.Now()).Render(r.Context(), w) +} +``` + +### Pattern 7: One `hx-get` demo (success criterion 3) +```html + + +
+ +``` +Server returns an HTML fragment (e.g. `2026-05-14T12:00:00Z`). Zero JS required client-side beyond HTMX. + +### Anti-Patterns to Avoid +- **Loading HTMX from a CDN.** D-10 mandates vendoring. CDN couples the app to network reachability and breaks the "single binary + static" thesis. +- **Spawning a new `pgxpool` per request.** One pool for the process lifetime; share via dependency injection. +- **Putting `templ generate` inside `go run`.** Run it via `just generate` (or air's `pre_cmd`) before the build — `.templ` files don't compile by themselves. +- **Logging the raw `Authorization` header or `Cookie` in the request logger.** Not in scope Phase 1 but the logger middleware should be written from day one with a known safe-fields list. +- **Using chi's `middleware.Logger`.** It writes plain text. Replace with a slog-backed middleware to keep one logging format. +- **Hardcoding port/DSN.** Read from env per D-15. + +## Don't Hand-Roll + +| Problem | Don't Build | Use Instead | Why | +|---------|-------------|-------------|-----| +| Connection pooling | Custom pool over `database/sql` | `pgxpool.Pool` | Battle-tested, exposed `Ping`/`Stat`/`Acquire` | +| Request IDs | Custom random string generator | `github.com/google/uuid` v1.6.0 | RFC 4122 conformant, collision-resistant | +| Middleware composition | Manual `http.Handler` wrapping | chi's `Use` chain | Order and short-circuiting handled correctly | +| Migrations | `psql -f` from a Makefile | `goose` | Version tracking, status, rollback support | +| HTML template typing | string concat or html/template | `templ` | Compile-time type safety prevents XSS by default | +| Live reload | shell loops + inotify | `air` | Handles partial-rebuild edge cases (panic/exit codes) | +| CSS build | Hand-curated CSS | Tailwind standalone CLI | Purges unused classes; consistent design tokens | +| Graceful shutdown wiring | Custom signal goroutine | `signal.Notify` + `http.Server.Shutdown` | Documented stdlib idiom | + +**Key insight:** Phase 1 is well-trodden territory. Every piece of the scaffold has a canonical Go ecosystem answer; deviation should require an explicit reason in the plan. + +## Runtime State Inventory + +> Phase 1 is a **greenfield** phase — `backend/` does not yet exist. No rename/refactor/migration involved. Section omitted. + +## Common Pitfalls + +### Pitfall 1: `templ generate` not run before `go build` +**What goes wrong:** `*.templ.go` files are missing; compilation fails with "undefined: templates.Index". +**Why it happens:** Templ files compile to Go via a separate generator. Devs forget after `git clone`. +**How to avoid:** +- Make `just generate` the first step in `just dev`, `just build`, and `just test`. +- Document the bootstrap order in README.md. +- Add `*.templ.go` to `.gitignore` so generated files are never committed (forces regeneration). +**Warning signs:** Fresh-clone "undefined" compile errors; CI failures only on first builds. + +### Pitfall 2: pgxpool dialing before Postgres is ready +**What goes wrong:** `just dev` starts the web binary before `compose up -d postgres` has finished initializing → first request returns 503. +**Why it happens:** `podman compose up -d` returns once the container exists, not once Postgres is accepting connections. +**How to avoid:** +- Use `compose.yaml` healthcheck (`pg_isready -U xtablo`) as in `go-backend/compose.yaml`. +- In `just dev`, wait for healthy before starting `air`, or accept that `/healthz` returns 503 for the first few seconds (this is actually correct behavior). +- pgxpool's `New` does not eagerly connect — connections are lazy. Don't try to "fix" this by adding a startup `Ping` retry loop. +**Warning signs:** Intermittent first-request 503s on cold starts. + +### Pitfall 3: Tailwind config doesn't see `.templ` files +**What goes wrong:** Tailwind purges all utility classes used only in `.templ` files → blank-looking page after CSS rebuild. +**Why it happens:** Tailwind v4 scans content paths declared in CSS (`@source`) or config. Default glob is `*.html` and `*.{js,ts,jsx,tsx}` — `.templ` is not included. +**How to avoid:** Add explicit content sources in `tailwind.input.css`: +```css +@source "../templates/**/*.templ"; +@source "../internal/web/**/*.go"; +``` +**Warning signs:** Classes work in `templ-generate`d Go files but disappear after Tailwind rebuild. + +### Pitfall 4: Forgetting to close `pgxpool` on shutdown +**What goes wrong:** Process exits with active connections in flight; Postgres logs `client unexpectedly closed`. +**Why it happens:** `os.Exit` after `http.Server.Shutdown` skips deferred `pool.Close()`. +**How to avoid:** Always call `pool.Close()` explicitly after `Shutdown` returns, not via `defer` from `main`. + +### Pitfall 5: air watching too much (or too little) +**What goes wrong:** Rebuilds loop on its own generated output (`*.templ.go`, `tailwind.css`) or fails to pick up `.templ` edits. +**Why it happens:** Default `air.toml` watches `.go` only and includes everything in `tmp/`. +**How to avoid:** Configure `.air.toml` explicitly: +- `include_ext = ["go", "templ"]` +- `exclude_dir = ["tmp", "bin", "static", ".git", "internal/db/sqlc"]` +- `exclude_regex = [".*_templ\\.go$"]` (generated files; let templ regenerate via pre-cmd, then air rebuilds) +- `pre_cmd = ["templ generate"]` + +### Pitfall 6: chi `middleware.Logger` clashes with slog +**What goes wrong:** Two log lines per request, one plain-text from chi, one structured from your slog middleware. +**Why it happens:** Adding `middleware.Logger` from chi alongside a custom slog logger. +**How to avoid:** Don't use chi's built-in Logger. Write a thin slog-backed alternative or use `github.com/go-chi/httplog/v2`. + +### Pitfall 7: Goose migration directory mismatch with sqlc +**What goes wrong:** sqlc generates code from a schema that doesn't match what goose has applied; queries fail at runtime. +**Why it happens:** sqlc's `schema` path doesn't include the goose migrations directory, or the order differs. +**How to avoid:** Point `sqlc.yaml` `schema` at `migrations/` directly so sqlc reads the same `.sql` files goose runs: +```yaml +sql: + - engine: postgresql + schema: "migrations" + queries: "internal/db/queries" + gen: { go: { sql_package: "pgx/v5", ... } } +``` +**Note:** Phase 1 has no queries yet — but get this config right now to avoid Phase 2 friction. + +### Pitfall 8: HTMX served from CDN by accident in the demo +**What goes wrong:** Copy-pasted HTMX example uses `