Config & Postgres (pgx, goose)
the server loads config from env, connects to Postgres via a pgxpool,
runs the first migration (the citizens table + extensions), and /healthz reports DB reachability.
Concepts — twelve-factor config, connection pooling, SQL migrations as versioned files, context deadlines on DB calls.
Build
internal/config/config.go:
package config
import (
"fmt"
"os"
"time"
)
type Config struct {
Addr string
DatabaseURL string // postgres://app:...@localhost:5432/bonikyc?sslmode=disable (dev)
ShutdownGrace time.Duration
// secrets (KEK passphrase, pepper) are loaded SEPARATELY in lab 7 — not here,
// and NEVER with a struct tag that could log them.
}
func Load() (Config, error) {
c := Config{
Addr: getenv("ADDR", ":8080"),
DatabaseURL: os.Getenv("DATABASE_URL"),
ShutdownGrace: 10 * time.Second,
}
if c.DatabaseURL == "" {
return c, fmt.Errorf("DATABASE_URL is required") // ⟵ YOU: fail fast on missing required config
}
return c, nil
}
func getenv(k, def string) string { if v := os.Getenv(k); v != "" { return v }; return def }migrations/00001_citizens.sql
migrations/00001_citizens.sql (goose format):
-- +goose Up
CREATE EXTENSION IF NOT EXISTS pgcrypto; -- gen_random_uuid()
CREATE TABLE citizens (
-- ⟵ YOU: transcribe from docs/01-data-model.md §3 (citizens table).
-- Type the columns yourself — it's how you internalize the encryption model.
);
-- +goose Down
DROP TABLE citizens;internal/store/db.go
DB wiring (put in internal/store/db.go):
func Open(ctx context.Context, url string) (*pgxpool.Pool, error) {
cfg, err := pgxpool.ParseConfig(url)
if err != nil { return nil, err }
cfg.MaxConns = 10 // ⟵ YOU: the mini PC isn't huge — research a sane pool size
cfg.MaxConnLifetime = time.Hour
pool, err := pgxpool.NewWithConfig(ctx, cfg)
if err != nil { return nil, err }
// ⟵ YOU: pool.Ping(ctx) with a short timeout; return error if unreachable.
return pool, nil
}Research checkpoints
install, goose create, up/down, and the -- +goose StatementBegin/End markers (you'll need them for trigger functions in lab 14). How does goose track applied versions?
why pgx native (not the database/sql adapter) for a Postgres-only app? (Types, performance, LISTEN/NOTIFY.)
create app and migrator Postgres roles per 01-data-model §4.4. app must lack DDL rights. Write the GRANTs. Confirm app cannot DROP TABLE.
fine as disable for localhost dev; what will it be once the DB is on the mini PC behind the app on the same host? (Trick question — think about the trust boundary B3.)
Verify
goose upcreatescitizensmatching the data-model doc exactly.- Server refuses to start with a clear error if
DATABASE_URLis unset. /healthzreturns{"status":"ok","db":"up"}; stop Postgres →"db":"down"
and a 503.
- Connecting as
appand runningDROP TABLE citizens;is denied.