Code

Wire the database in — main, handlers, home

Now connect the pieces. Three modifications, as tabs, following the data from its source outward: main.gohandlers.gohome.templ (start the DB, give handlers the queries, show the count). The diffs show only what changed; everything from lessons 1–2 is carried forward.

1. cmd/signflow/main.go (modify) — two steps slot in between config and serving: run the migrations, then open the pool.

if err := runMigrations(cfg.DatabaseURL, log); err != nil {   // goose Up, embedded
	return err
}
pool, err := db.Connect(ctx, cfg.DatabaseURL)                 // pgx pool
// ...
queries := db.New(pool)
h := handlers.New(cfg, queries, log)                          // ← now takes queries

runMigrations uses goose's database/sql handle (via the pgx stdlib driver) to apply the embedded migrations, then the app runs on a pgx pool. The static/serving/shutdown code below is unchanged.

2. internal/handlers/handlers.go (modify) — the struct gains a Queries field, New takes it, and Home reads the count:

count, err := h.Queries.CountUsers(r.Context())
// ...
render(w, r, http.StatusOK, web.Home(count))

Health now pings the DB too — an unreachable database reports 503 unhealthy instead of silently serving 500s.

3. internal/web/home.templ (modify) — Home grows a parameter and a line:

templ Home(userCount int64) {          // ← was Home()
	...
	Registered users in the database: <strong>{ strconv.FormatInt(userCount, 10) }</strong>

Verify. Create the database, then run (remember: templ generate for the home change, sqlc generate if you haven't):

createdb signflow
templ generate
go run ./cmd/signflow

Startup now logs the migration applying:

level=INFO msg="starting signflow" env=dev port=8080 base_url=http://localhost:8080
level=INFO msg=goose msg="OK   00001_users.sql"
level=INFO msg=listening addr=:8080

Open http://localhost:8080 — the status card now reads "Registered users in the database: 0", live from Postgres. Insert one by hand and reload:

$ psql signflow -c "INSERT INTO users (email, password_hash) VALUES ('a@b.co', 'x');"
INSERT 0 1

Reload the page → "Registered users in the database: 1". The count came from the database, through your sqlc-generated CountUsers, into a type-safe template. The whole stack is wired.

Gotcha (migration already applied). Run go run twice and the second start logs no OK 00001_users.sql — goose records applied migrations in a goose_db_version table and skips them. That's the point: migrations are apply-once and safe to run on every boot. To start clean, dropdb signflow && createdb signflow.