Code

Hashing, the session store, the Manager

First the new dependency (bcrypt lives in the Go team's extended crypto):

go get golang.org/x/crypto/bcrypt

Five files, as tabs, in dependency order: 00002_sessions.sqlsessions.sqlusers.sqlpassword.gosession.go (the table, its queries, the user queries register needs, the hasher, then the Manager that ties sessions together). Then run sqlc generate so the new queries become Go.

1. db/migrations/00002_sessions.sql — the session store. Note what's here and what isn't:

token_hash TEXT NOT NULL UNIQUE,                        -- we store the HASH, not the token
user_id    UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
expires_at TIMESTAMPTZ NOT NULL

ON DELETE CASCADE means deleting a user removes their sessions automatically. token_hash is UNIQUE (one token → one session) and, crucially, it's the hash — the raw token never touches the database.

2. internal/db/queries/sessions.sqlCreateSession, GetUserBySessionToken (a join that resolves a token straight to a user, and filters expires_at > now()), and DeleteSession (logout, next lesson). GetUserBySessionToken uses sqlc.embed(users) so it returns a whole typed User.

3. internal/db/queries/users.sql (modify) — register needs two more queries: CreateUser (insert, RETURNING *) and GetUserByEmail (the duplicate check + login, next lesson). CountUsers from lesson 3 is untouched — the diff is pure additions.

4. internal/auth/password.goHashPassword / CheckPassword. The length cap is the interesting line:

// MaxPasswordLen guards bcrypt's 72-byte input limit so passwords are never
// silently truncated. (A byte, not a rune, but 64 chars is plenty.)
MaxPasswordLen = 64

CheckPassword uses bcrypt.CompareHashAndPassword, which is constant-time — it can't be used as a timing oracle to guess the password.

5. internal/session/session.go — the Manager: Create (mint a random token, store its hash, set the cookie), User (cookie → hash → user, or ErrNoSession), Destroy (delete the row + expire the cookie). The cookie helper is where the flags live:

HttpOnly: true,                 // JS can't read it — mitigates XSS token theft
Secure:   m.secure,             // HTTPS-only in production (cfg.IsProd())
SameSite: http.SameSiteLaxMode, // sent on top-level navigations, not cross-site POSTs

Create is used this lesson (register); User and Destroy are the machinery lesson 5 will call for login and logout — written now because they're one cohesive unit.