Skip to content

Getting started

  • Go 1.27 or newer — the code uses slog.DiscardHandler and 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: kubectl and helm, pointed at a cluster with an ingress controller and a container registry it can pull from.

The run target starts a throwaway PostgreSQL container first, so a clean checkout comes up with one command:

5432/labapp
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:

Terminal window
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.

One target runs everything that must pass before code is committed — formatting, go vet, the linter, and the unit tests under the race detector:

Terminal window
make check

A 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:

Terminal window
make test-integration
# --- PASS: TestMigrateIsIdempotent (0.05s)
# --- PASS: TestUserLifecycle (0.02s)
# --- PASS: TestSessionLifecycle (0.02s)
# --- PASS: TestExpiredSessionIsInvisible (0.02s)

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:

Terminal window
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:

Terminal window
make acceptance
# ACCEPTANCE PASS