go-htmx-gsd #1
1 changed files with 799 additions and 0 deletions
799
.planning/phases/01-foundation/01-RESEARCH.md
Normal file
799
.planning/phases/01-foundation/01-RESEARCH.md
Normal file
|
|
@ -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>
|
||||
## 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/<domain>` 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).
|
||||
</user_constraints>
|
||||
|
||||
<phase_requirements>
|
||||
## 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) |
|
||||
</phase_requirements>
|
||||
|
||||
## 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 <module>` 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 + <head>
|
||||
│ ├── 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
|
||||
<!-- templates/index.templ renders this -->
|
||||
<button hx-get="/demo/time" hx-target="#demo-out" hx-swap="innerHTML">
|
||||
What time is it?
|
||||
</button>
|
||||
<div id="demo-out"></div>
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
```
|
||||
Server returns an HTML fragment (e.g. `<span>2026-05-14T12:00:00Z</span>`). 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 `<script src="https://unpkg.com/htmx.org">` instead of `/static/htmx.min.js`.
|
||||
**Why it happens:** Every HTMX tutorial uses unpkg.
|
||||
**How to avoid:** Vendor `htmx.min.js` (the file is ~50KB) into `static/` during `just bootstrap`. Reference it via `/static/htmx.min.js`.
|
||||
|
||||
## Code Examples
|
||||
|
||||
Verified patterns from official sources:
|
||||
|
||||
### `/healthz` handler (FOUND-01, success criterion 2)
|
||||
```go
|
||||
// Source: derived from pgxpool docs + chi router pattern
|
||||
func healthzHandler(pool *pgxpool.Pool) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
|
||||
defer cancel()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := pool.Ping(ctx); 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"})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Goose migration file (`migrations/0001_init.sql`)
|
||||
```sql
|
||||
-- Source: github.com/pressly/goose README format
|
||||
-- +goose Up
|
||||
-- Phase 1: no-op bootstrap migration. Schema lands in Phase 2.
|
||||
SELECT 1;
|
||||
|
||||
-- +goose Down
|
||||
SELECT 1;
|
||||
```
|
||||
|
||||
### justfile recipes (FOUND-05)
|
||||
```makefile
|
||||
# Reference: existing go-backend/justfile (adapted: pnpm removed)
|
||||
set shell := ["bash", "-cu"]
|
||||
database_url := "postgres://xtablo:xtablo@localhost:5432/xtablo?sslmode=disable"
|
||||
tailwind := "./bin/tailwindcss"
|
||||
|
||||
default:
|
||||
@just --list
|
||||
|
||||
bootstrap:
|
||||
# Downloads tailwind standalone to ./bin (see Pitfall 8 + D-12)
|
||||
mkdir -p bin
|
||||
curl -sSL -o bin/tailwindcss \
|
||||
https://github.com/tailwindlabs/tailwindcss/releases/download/v4.X.X/tailwindcss-$(uname -s | tr A-Z a-z)-$(uname -m)
|
||||
chmod +x bin/tailwindcss
|
||||
|
||||
db-up:
|
||||
podman compose up -d postgres
|
||||
|
||||
db-down:
|
||||
podman compose down
|
||||
|
||||
migrate cmd="status":
|
||||
GOOSE_DRIVER=postgres GOOSE_DBSTRING='{{database_url}}' GOOSE_MIGRATION_DIR=migrations \
|
||||
goose {{cmd}}
|
||||
|
||||
generate:
|
||||
templ generate
|
||||
sqlc generate
|
||||
{{tailwind}} -i tailwind.input.css -o static/tailwind.css
|
||||
|
||||
styles-watch:
|
||||
{{tailwind}} -i tailwind.input.css -o static/tailwind.css --watch
|
||||
|
||||
dev: db-up
|
||||
just generate
|
||||
DATABASE_URL='{{database_url}}' air -c .air.toml
|
||||
|
||||
test:
|
||||
just generate
|
||||
go test ./...
|
||||
|
||||
lint:
|
||||
go vet ./...
|
||||
gofmt -l . | (grep . && exit 1 || exit 0)
|
||||
|
||||
build:
|
||||
just generate
|
||||
go build -o bin/web ./cmd/web
|
||||
go build -o bin/worker ./cmd/worker
|
||||
```
|
||||
|
||||
### `compose.yaml` (lifted from go-backend with seed mount removed)
|
||||
```yaml
|
||||
# Source: existing go-backend/compose.yaml — strip dev seed mounts for Phase 1
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: xtablo-backend-postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: xtablo
|
||||
POSTGRES_USER: xtablo
|
||||
POSTGRES_PASSWORD: xtablo
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U xtablo -d xtablo"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
```
|
||||
|
||||
### `.env.example`
|
||||
```bash
|
||||
# Source: D-15
|
||||
DATABASE_URL=postgres://xtablo:xtablo@localhost:5432/xtablo?sslmode=disable
|
||||
PORT=8080
|
||||
ENV=development
|
||||
```
|
||||
|
||||
### `.air.toml` (key sections)
|
||||
```toml
|
||||
# Source: github.com/air-verse/air README, adapted for templ
|
||||
root = "."
|
||||
tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
cmd = "templ generate && go build -o ./tmp/web ./cmd/web"
|
||||
bin = "./tmp/web"
|
||||
include_ext = ["go", "templ"]
|
||||
exclude_dir = ["tmp", "bin", "static", ".git", "internal/db/sqlc"]
|
||||
exclude_regex = [".*_templ\\.go$"]
|
||||
delay = 200
|
||||
stop_on_error = true
|
||||
```
|
||||
|
||||
## State of the Art
|
||||
|
||||
| Old Approach | Current Approach | When Changed | Impact |
|
||||
|--------------|------------------|--------------|--------|
|
||||
| `cosmtrek/air` import path | `air-verse/air` | Repo transferred 2024 | Use new path in `go install`; old path still redirects |
|
||||
| `lib/pq` Postgres driver | `pgx/v5 + pgxpool` | pgx v5 release (2022) | Standard for new Go services; richer pg types, better perf |
|
||||
| `database/sql` + handcrafted queries | `sqlc` codegen | Stable since ~2020 | Type-safe SQL without ORM overhead |
|
||||
| `html/template` | `a-h/templ` | Templ stable for ~2 years | Compile-time type checking; first-class HTMX fragment story |
|
||||
| Tailwind via PostCSS + Node | Tailwind standalone CLI (v4) | Tailwind v4 (2024) | Pure binary; no JS toolchain dependency |
|
||||
| logrus / zap | `log/slog` (stdlib) | Go 1.21 (Aug 2023) | Stdlib structured logging; no external dep needed for greenfield |
|
||||
|
||||
**Deprecated/outdated:**
|
||||
- `cosmtrek/air` URL (still works, but canonical is `air-verse/air`).
|
||||
- `lib/pq` for new code (still maintained but pgx is preferred for new services).
|
||||
- Any tutorial that uses `pq.NewConnector` or `sql.Open("postgres", ...)` — translate to pgx.
|
||||
|
||||
## Assumptions Log
|
||||
|
||||
| # | Claim | Section | Risk if Wrong |
|
||||
|---|-------|---------|---------------|
|
||||
| A1 | HTMX latest stable is v2.x — vendor whatever the latest 2.x `htmx.min.js` is at execution time | Standard Stack | Low. If v3 ships before execution, planner should re-check API compatibility for `hx-get`/`hx-target` (no breaking changes expected for these core attributes). |
|
||||
| A2 | Tailwind v4.x is the current major; pin a specific 4.x patch in the justfile during execution | Standard Stack, Code Examples | Low. v3 → v4 introduced `@source` directive; if v4 is not available on a platform binary, fall back to v3 syntax. |
|
||||
| A3 | Go toolchain available on developer machine is ≥ 1.22 (chi v5.2+ requirement) | Standard Stack | Existing `go-backend/go.mod` declares `go 1.26.0`, so this is highly likely. Verify in Environment Availability step. |
|
||||
| A4 | `podman compose` v2 syntax matches `docker compose` v2 syntax for the services used here | Architecture | Low. Both implement the Compose spec; `postgres:16-alpine` is universally supported. |
|
||||
|
||||
**Interpretation:** All four assumptions are LOW risk; planner can proceed without user confirmation but should re-verify HTMX/Tailwind versions at the moment of bootstrap.
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Goose: pure CLI in Phase 1, or library-call from a `backend migrate` subcommand now?**
|
||||
- What we know: D-04 + D-05 say CLI for Phase 1; D-71 (Claude's discretion) says planner may set up for Phase 7's likely library approach.
|
||||
- What's unclear: Whether to add the `cmd/migrate/main.go` (or `cmd/web` subcommand) skeleton now or wait.
|
||||
- Recommendation: Pure CLI in Phase 1. Adding the library wiring now blurs the phase boundary and creates two migration paths (CLI + lib) before one is hardened. Phase 7 can introduce `cmd/web migrate` cleanly.
|
||||
|
||||
2. **Tailwind watch: separate `just styles` process, or air `pre_cmd`?**
|
||||
- What we know: D-14 leaves this to the planner.
|
||||
- What's unclear: Whether developers run `just dev` (one terminal) or `just dev` + `just styles` (two terminals).
|
||||
- Recommendation: Two terminals — keep concerns separated. Tailwind watch is independent of Go rebuild; piping through air's `pre_cmd` would trigger CSS rebuild on every `.go` save, which is wasteful. Document the two-terminal workflow in README.
|
||||
|
||||
3. **Phase 1 `/healthz` smoke test: ship it or defer?**
|
||||
- What we know: Phase 2 establishes the testing strategy.
|
||||
- What's unclear: Whether a single `handlers_test.go` covering `/healthz` (with a stub `Pinger` interface) earns its keep now.
|
||||
- Recommendation: Ship it. It exercises the chi+slog wiring, provides a regression net for Phase 2 to extend, and the `Pinger` interface stub costs <10 lines. See Validation Architecture below.
|
||||
|
||||
## Environment Availability
|
||||
|
||||
| Dependency | Required By | Available | Version | Fallback |
|
||||
|------------|------------|-----------|---------|----------|
|
||||
| Go toolchain | Build/run web+worker | ✓ (existing `go-backend` uses 1.26) | 1.26 | — |
|
||||
| `just` | All recipes | ✓ (used by existing go-backend) | latest | — |
|
||||
| `podman` + `podman compose` | Local Postgres | ✓ (developer standard per D-11) | latest | docker compose with same compose.yaml |
|
||||
| `git` | Repo ops | ✓ | — | — |
|
||||
| `curl` | `just bootstrap` (tailwind download) | ✓ (macOS/Linux standard) | — | `wget` |
|
||||
| `goose` CLI | `just migrate` | ✗ (likely missing) | — | `go install github.com/pressly/goose/v3/cmd/goose@v3.27.1` in `just bootstrap` |
|
||||
| `templ` CLI | `just generate` | ✗ (likely missing) | — | `go install github.com/a-h/templ/cmd/templ@v0.3.1020` in `just bootstrap` |
|
||||
| `sqlc` CLI | `just generate` | ✗ (likely missing) | — | `go install github.com/sqlc-dev/sqlc/cmd/sqlc@v1.31.1` in `just bootstrap` |
|
||||
| `air` CLI | `just dev` | ✗ (likely missing) | — | `go install github.com/air-verse/air@v1.65.1` in `just bootstrap` |
|
||||
| Tailwind standalone | CSS build | ✗ (missing) | — | `just bootstrap` downloads pinned version to `./bin/tailwindcss` |
|
||||
| HTMX `htmx.min.js` | Demo page | ✗ (missing) | — | `just bootstrap` curls latest 2.x release into `static/htmx.min.js` |
|
||||
|
||||
**Missing dependencies with no fallback:** None. Every missing tool has an install path.
|
||||
|
||||
**Missing dependencies with fallback:** All Phase 1 tools (goose, templ, sqlc, air, tailwind, htmx) are bootstrappable via `just bootstrap`. Plan must include this recipe.
|
||||
|
||||
## Validation Architecture
|
||||
|
||||
### Test Framework
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Framework | Go stdlib `testing` (Go 1.26) |
|
||||
| Config file | none (Go convention) |
|
||||
| Quick run command | `go test ./internal/web/...` |
|
||||
| Full suite command | `just test` (runs `just generate` then `go test ./...`) |
|
||||
|
||||
### Phase Requirements → Test Map
|
||||
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|
||||
|--------|----------|-----------|-------------------|-------------|
|
||||
| FOUND-01 | `cmd/web` builds; `cmd/worker` builds | smoke (build) | `go build ./cmd/web ./cmd/worker` | ❌ Wave 0 |
|
||||
| FOUND-01 | `/healthz` returns 200 with `{"status":"ok","db":"ok"}` when DB up | unit (handler + stub Pinger) | `go test ./internal/web -run TestHealthz_OK` | ❌ Wave 0 |
|
||||
| FOUND-01 | `/healthz` returns 503 with `{"status":"degraded","db":"down"}` when DB down | unit (handler + stub Pinger returns error) | `go test ./internal/web -run TestHealthz_Down` | ❌ Wave 0 |
|
||||
| FOUND-02 | `pgxpool.New` succeeds against compose Postgres | integration (manual or local-only) | `go test ./internal/db -run TestPool_Connects` (skipped if `DATABASE_URL` unset) | ❌ Wave 0 |
|
||||
| FOUND-02 | `goose up` applies bootstrap migration cleanly | manual (justfile execution) | `just migrate up && just migrate status` | manual |
|
||||
| FOUND-03 | Root route returns 200 with HTML containing `hx-get` attribute | unit (httptest) | `go test ./internal/web -run TestIndex_RendersHxGet` | ❌ Wave 0 |
|
||||
| FOUND-03 | `/demo/time` returns HTML fragment | unit (httptest) | `go test ./internal/web -run TestDemoTime_Fragment` | ❌ Wave 0 |
|
||||
| FOUND-03 | `just dev` live-reloads on `.go` and `.templ` edits | manual | manual visual check | manual |
|
||||
| FOUND-04 | RequestID middleware sets `X-Request-ID` header | unit (httptest + middleware chain) | `go test ./internal/web -run TestRequestID_HeaderSet` | ❌ Wave 0 |
|
||||
| FOUND-04 | slog handler is JSON when `ENV=production`, text otherwise | unit (capture handler output) | `go test ./internal/web -run TestSlog_HandlerSwitch` | ❌ Wave 0 |
|
||||
| FOUND-04 | Graceful shutdown closes pgxpool | manual or signal-driven test | `go test ./internal/web -run TestShutdown_ClosesPool` (optional; manual acceptable) | manual |
|
||||
| FOUND-05 | `.env.example` exists and contains `DATABASE_URL`, `PORT`, `ENV` | smoke (file existence + grep) | `test -f .env.example && grep -q DATABASE_URL .env.example` | manual |
|
||||
| FOUND-05 | `compose.yaml` brings up Postgres healthily | manual | `just db-up && podman compose ps` | manual |
|
||||
| FOUND-05 | `justfile` exposes `dev`, `migrate`, `test`, `lint` | smoke | `just --list \| grep -E '^(dev\|migrate\|test\|lint)\b'` | manual |
|
||||
| FOUND-05 | README quickstart works in <5min on fresh clone | manual (peer review) | dev follows README | manual |
|
||||
|
||||
### Sampling Rate
|
||||
- **Per task commit:** `go test ./internal/web/...` (fast: <2s for the listed unit tests)
|
||||
- **Per wave merge:** `just test` (runs `just generate` then `go test ./...`)
|
||||
- **Phase gate:** `just check` equivalent (`generate` + `test` + `build`) green, plus the manual checks (compose up, just dev visual, README walkthrough) before `/gsd-verify-work`.
|
||||
|
||||
### Wave 0 Gaps
|
||||
- [ ] `backend/internal/web/handlers_test.go` — covers FOUND-01 (/healthz OK + down), FOUND-03 (index + fragment), FOUND-04 (RequestID, slog switch)
|
||||
- [ ] `backend/internal/db/pool_test.go` — covers FOUND-02 (pgxpool connects); skip when `DATABASE_URL` unset for CI
|
||||
- [ ] Shared test helpers (`stubPinger` implementing a `Pinger` interface) live in `internal/web/handlers_test.go`
|
||||
- [ ] No framework install needed (stdlib `testing` only)
|
||||
- [ ] `.github/` or similar CI config is **out of scope** for Phase 1 (no DEPLOY-XX requirement applies). Local `just test` is the gate.
|
||||
|
||||
## Security Domain
|
||||
|
||||
> Phase 1 introduces no authentication, no user input handling, and no data persistence beyond a no-op migration. ASVS scope is therefore minimal. Full security controls land in Phase 2 (AUTH-01..07). The notes below establish the *foundation hooks* Phase 2 will use.
|
||||
|
||||
### Applicable ASVS Categories
|
||||
|
||||
| ASVS Category | Applies | Standard Control |
|
||||
|---------------|---------|-----------------|
|
||||
| V2 Authentication | no (Phase 2) | — |
|
||||
| V3 Session Management | no (Phase 2) | — |
|
||||
| V4 Access Control | no (Phase 3 owners-only) | — |
|
||||
| V5 Input Validation | partial | No user input in Phase 1 surfaces. When `/demo/time` accepts no params, no validation required. templ auto-escapes interpolations — preserves this property for future phases. |
|
||||
| V6 Cryptography | no | No secrets handled in Phase 1 beyond `DATABASE_URL` in env. |
|
||||
| V7 Error Handling & Logging | yes | slog with RequestID; **do not log** `DATABASE_URL`, raw headers, or full request bodies. |
|
||||
| V8 Data Protection | partial | `.env` must be in `.gitignore`; `.env.example` is the only env file committed. |
|
||||
| V14 Configuration | yes | Env-driven config; `.env.example` documents required keys; no secrets in repo. |
|
||||
|
||||
### Known Threat Patterns for {Go web + Postgres + HTMX}
|
||||
|
||||
| Pattern | STRIDE | Standard Mitigation |
|
||||
|---------|--------|---------------------|
|
||||
| XSS via unescaped HTML | Tampering | templ auto-escapes by default; never use `templ.Raw` on user data (no user data in Phase 1) |
|
||||
| SQL injection | Tampering | sqlc + pgx parameterized queries (no queries in Phase 1, but the wiring exists) |
|
||||
| Information disclosure via logs | Information disclosure | slog handler with explicit field allowlist; never log full Authorization/Cookie headers |
|
||||
| Open file traversal via `/static/` | Tampering | `http.FileServer` over an `http.Dir("static")` rooted at a single dir is safe; do **not** use `http.StripPrefix` incorrectly such that paths can escape |
|
||||
| Secret commit (`.env`) | Information disclosure | `.gitignore` includes `.env`; `.env.example` only |
|
||||
| Panic → process crash | DoS | `middleware.Recoverer` recovers panics; structured log + 500 response |
|
||||
| Slow client → resource exhaustion | DoS | `http.Server.ReadTimeout`, `WriteTimeout`, `IdleTimeout` set explicitly (Phase 1 should set sane defaults, e.g., 15s/15s/60s) |
|
||||
|
||||
## Sources
|
||||
|
||||
### Primary (HIGH confidence)
|
||||
- pressly/goose releases page (v3.27.1, Apr 24): https://github.com/pressly/goose/releases
|
||||
- a-h/templ releases (v0.3.1020, May 10): https://github.com/a-h/templ/releases
|
||||
- go-chi/chi releases (v5.2.5, Feb 5): https://github.com/go-chi/chi/releases
|
||||
- jackc/pgx tags (v5.9.2, Apr 19, 2026): https://github.com/jackc/pgx/tags
|
||||
- sqlc-dev/sqlc releases (v1.31.1, Apr 22): https://github.com/sqlc-dev/sqlc/releases
|
||||
- air-verse/air releases (v1.65.1, Apr 12): https://github.com/air-verse/air/releases
|
||||
- Tailwind standalone CLI announcement: https://tailwindcss.com/blog/standalone-cli
|
||||
- pressly/goose README (CLI commands, file format): https://github.com/pressly/goose
|
||||
- `log/slog` package docs: https://pkg.go.dev/log/slog
|
||||
- `jackc/pgx/v5/pgxpool` docs: https://pkg.go.dev/github.com/jackc/pgx/v5/pgxpool
|
||||
- templ guide (HTTP integration): https://templ.guide
|
||||
- Existing `go-backend/` (verified working reference for templ+chi+pgx+sqlc+podman compose wiring)
|
||||
|
||||
### Secondary (MEDIUM confidence)
|
||||
- chi middleware order convention — derived from chi v5 README + community-standard examples.
|
||||
- HTMX v2 attribute set (`hx-get`, `hx-target`, `hx-swap`) — stable since v1; backward-compatible in v2 for core attributes.
|
||||
|
||||
### Tertiary (LOW confidence)
|
||||
- Tailwind v4 `@source` directive syntax — should be verified at execution time against the pinned Tailwind version (v3 used `content` arrays in `tailwind.config.js`).
|
||||
- Exact HTMX 2.x patch version — vendor latest at execution time.
|
||||
|
||||
## Metadata
|
||||
|
||||
**Confidence breakdown:**
|
||||
- Standard stack: HIGH — every version verified against GitHub releases / tags.
|
||||
- Architecture: HIGH — locked in CONTEXT.md; patterns match existing `go-backend/` reference and Go ecosystem canon.
|
||||
- Pitfalls: HIGH — drawn from canonical docs and the existing `go-backend/` pitfalls (e.g., templ generate ordering already proven in that scaffold).
|
||||
- Validation strategy: HIGH — stdlib `testing` is the only sane choice for Go; Wave 0 file list is concrete.
|
||||
- Security: HIGH-for-Phase-1-scope — Phase 1 has minimal attack surface; real security work is Phase 2.
|
||||
|
||||
**Research date:** 2026-05-14
|
||||
**Valid until:** 2026-06-14 (30 days; stack is stable, but re-verify Tailwind/HTMX/air versions if execution slips past this).
|
||||
Loading…
Reference in a new issue