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 `