Code

The signers table and the transaction plumbing

Five files as tabs, bottom-up: the table, its queries, the status-flip query, and the transaction plumbing. 00005_signers.sqlsigners.sqldocuments.sqlhandlers.gomain.go.

1. db/migrations/00005_signers.sql (new) — the signers table. Key columns: document_id (FK, ON DELETE CASCADE), email, token_hash TEXT NOT NULL UNIQUE, status (pending/signed), the three signature columns (signed_name, signed_at, signed_doc_hash — filled next lesson), and expires_at. The UNIQUE (document_id, email) stops inviting the same person to the same document twice. token_hash UNIQUE is what makes a token map to exactly one row.

2. internal/db/queries/signers.sql (new) — two queries for now: CreateSigner (insert, RETURNING *) and ListSignersByDocument (the roster). The sign + count queries arrive next lesson. Run sqlc generate.

3. internal/db/queries/documents.sql (modify) — one new query, MarkDocumentSent: UPDATE ... SET status='sent' WHERE id=$1 AND owner_id=$2 AND status='draft'. Owner-scoped and draft-guarded — it can't flip a document that isn't yours or isn't a draft.

4. internal/handlers/handlers.go (modify) — the Handlers struct gains Pool *pgxpool.Pool, and New takes it. The query methods run through Queries; the raw Pool is only for opening transactions (pool.Begin). This is the first handler that needs more than one write to be atomic.

5. cmd/signflow/main.go (modify) — pass the pool you already opened to New:

h := handlers.New(cfg, pool, queries, sessions, mailer, store, log)

pool is the same *pgxpool.Pool the queries are built on; now the handlers hold it directly too, so they can open a transaction when one write is not enough.