Getting started
Requirements
Section titled “Requirements”- Go 1.27 or newer — the code uses
slog.DiscardHandlerand range-over-int, both recent additions. - Docker, for the local database and for building images. The service itself is built inside a container on deploy, so a working Go toolchain is only needed for tests and the editor.
- golangci-lint, invoked by the commit gate.
- For deploying:
kubectlandhelm, pointed at a cluster with an ingress controller and a container registry it can pull from.
Run it locally
Section titled “Run it locally”The run target starts a throwaway PostgreSQL container first, so a clean
checkout comes up with one command:
make run# level=INFO msg=starting app=labapp version=dev addr=:8080 env=dev# level=INFO msg="database ready, migrations applied"Migrations are embedded in the binary and applied at startup, so there is no separate schema step — the first run creates the tables.
Check it is alive, and that it can reach its database:
curl -s localhost:8080/healthz && curl -s localhost:8080/readyz# {"status":"ok"}# {"status":"ready"}/healthz answers as long as the process is running; /readyz also pings the
database. The distinction matters in Kubernetes: a pod that cannot reach
Postgres should be taken out of the load-balancer rotation, not restarted.
The commit gate
Section titled “The commit gate”One target runs everything that must pass before code is committed — formatting,
go vet, the linter, and the unit tests under the race detector:
make checkA git hook runs a faster subset on every commit. The full gate is cheap enough (a few seconds) that there is no reason to skip it.
Tests come in two flavours. The unit tests need nothing and cover the domain logic — password hashing, session rules, the rate limiter’s arithmetic. The integration tests exercise the real SQL against a disposable PostgreSQL container, which is the only way to catch the things that matter at that layer:
make test-integration# --- PASS: TestMigrateIsIdempotent (0.05s)# --- PASS: TestUserLifecycle (0.02s)# --- PASS: TestSessionLifecycle (0.02s)# --- PASS: TestExpiredSessionIsInvisible (0.02s)Deploying
Section titled “Deploying”make deploy builds an image tagged with the commit and a timestamp, pushes it,
upgrades the release, and then polls the live URL until that exact build
answers before reporting success. Deploying :latest and hoping is the
failure mode it exists to prevent:
make deploy# LIVE: https://labapp.example (a07c0b8f-20260902142704)Finally, an end-to-end script drives the whole auth flow against the running deployment — signup, a duplicate rejection, a bad password, a real login, the session used both ways, logout, and a confirmation that the revoked token is dead:
make acceptance# ACCEPTANCE PASS