Day 62 — Stage VI gate

Updated

July 30, 2026

Day 62 — Stage VI gate

Stage VI · Gate day · ~3h (integration / ship)
Goal: Close the data & APIs stage by shipping a service + persistent store + auth + tests, with a clear architecture README and operational basics from Days 51–61.

Important

Gates are pass/fail against a rubric. A pretty half-wired API fails. A small vertical slice that persists, authenticates, and tests cleanly passes.

Why this day exists

Stage VI introduced persistence, REST (or gRPC branch), auth, validation, caching, realtime, TLS literacy, and integration tests. The gate asks: can you assemble a credible vertical slice?

Not every elective must be in the gate binary—but core bar items must. Stage VII (build tags, pprof, containers, metrics, govulncheck) will profile and package this service. Spaghetti now becomes expensive later.


Theory 1 — Gate bar (definition of done)

Requirement Evidence
Persistent store via database/sql Migrations + repo; data survives restart
Versioned schema migrations Idempotent apply (up files or embed)
HTTP JSON API under /v1 (or gRPC + documented HTTP health) Routes work end-to-end
Auth protecting at least one mutating route 401 without creds
Consistent error envelope Day 55 shape (code/message/details)
Integration or solid repo tests + handler tests go test green on Go 1.26
Graceful shutdown SIGINT/SIGTERM → clean exit
slog logging Structured request or lifecycle logs
README architecture paragraph Trust boundaries, boxes
Race clean on unit/integration you claim -race where applicable

Explicit non-goals

  • Perfect OpenAPI client SDK
  • Multi-region HA
  • Full OAuth provider
  • Every Stage VI elective feature in one binary

Theory 2 — Vertical slice over horizontal sprawl

Prefer:

  • One resource fully done (users + items with ownership)

Over:

  • Five half-wired resources

Gate reviewers (including future you) should:

  1. Register / login
  2. Create an item (authenticated)
  3. Fetch it after process restart (persistence!)
  4. See tests pass

Trust boundary diagram (required)

[Client]
   |
   v
[http.Server + timeouts]
   |
   +--> [auth middleware] -- 401 if missing/invalid
   |
   v
[handlers] -- validate -- map domain errors --> error envelope
   |
   v
[app/service] -- business rules
   |
   +--> [repo] --> [sql.DB] --> [SQLite|Postgres]
   |
   +--> [cache?] (optional, in-process)

Theory 3 — Architecture paragraph (README)

Write in README (replace placeholders with yours):

## Architecture

Clients call the HTTP API (`/v1/*`). Authentication uses <JWT|sessions>.
Handlers validate input, call repository interfaces (or app services), and map
domain errors to HTTP status + a consistent error envelope. `database/sql`
talks to <SQLite|Postgres> with versioned SQL migrations applied at startup
(or via a migrate command). Optional in-process cache sits in front of hot
item reads. TLS is <terminated at the app|terminated at the proxy>.
Realtime updates use <SSE|WebSocket|none — skipped because ...>.

Include the text diagram above (adapted). Stage VII reviewers will re-read this when wiring metrics and containers.


Theory 4 — Suggested package layout

cmd/service/main.go          # composition root, signals, listen
internal/httpapi/            # handlers, middleware, routes
internal/auth/               # password hash, token/session
internal/store/              # sql repo
internal/migrate/            # apply versioned SQL
internal/cache/              # optional
internal/domain/             # errors, entities (if extracted)
migrations/*.sql
scripts/demo.sh
README.md

internal/ prevents external import of private code—good monorepo hygiene.

Composition root sketch

func main() {
    cfg := config.Load()
    log := slog.New(slog.NewJSONHandler(os.Stdout, nil))

    db, err := sql.Open(cfg.Driver, cfg.DSN)
    must(err)
    defer db.Close()
    must(migrate.Up(context.Background(), db))

    repo := store.NewItemRepo(db)
    auth := auth.NewService(cfg.JWTSecret, repo /* users */)
    api := httpapi.New(log, auth, repo)

    srv := &http.Server{
        Addr:              cfg.Addr,
        Handler:           api.Routes(),
        ReadHeaderTimeout: 5 * time.Second,
    }

    ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
    defer stop()

    go func() {
        log.Info("listen", "addr", cfg.Addr)
        if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
            log.Error("serve", "err", err)
            stop()
        }
    }()

    <-ctx.Done()
    shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    _ = srv.Shutdown(shutdownCtx)
}

Theory 5 — Auth and error envelope bars

Auth minimum

Check Pass looks like
Register Hashed password stored (bcrypt/argon2/scrypt)
Login Returns token or session cookie
Mutating route Create item without creds → 401
Ownership (bonus) User A cannot mutate User B’s item → 403/404

Error envelope minimum (Day 55 shape)

{
  "error": {
    "code": "not_found",
    "message": "item not found"
  }
}

Map domain sentinels consistently:

Domain HTTP
validation 400
unauthenticated 401
forbidden 403
not found 404
conflict 409
internal 500 (no stack traces to client)

Theory 6 — Rubric (score yourself)

Criterion Weight 0 1 2
Persistence + migrations 20% memory only SQL flaky restart-safe
Auth on mutating route 15% none partial solid 401
API + error envelope 15% ad-hoc inconsistent Day 55 shape
Tests (unit/handler/integ) 15% none thin green suite
Shutdown + slog 10% hang/noisy partial clean
README architecture 10% missing thin diagram+paragraph
Race / quality 10% fails race untested -race green
Demo script 5% none manual only demo.sh works

Pass: ≥ 80% with no zero on persistence or auth.


Lab 1 — Promote or assemble the gate module

mkdir -p ~/lab/90daysofx/01-go/day62
cd ~/lab/90daysofx/01-go/day62
# or: copy/promote day61 service and tag as day62
go mod init example.com/day62
# ensure: go 1.26

Checklist before demos:

  1. Migrations apply on empty DB.
  2. Register + login work.
  3. Create + get item works.
  4. Kill process; restart; get still works.
  5. go test ./... and go test -race ./... as applicable.

Lab 2 — Demo script scripts/demo.sh

#!/usr/bin/env bash
set -euo pipefail
base="${BASE_URL:-http://127.0.0.1:8080}"

curl -sf "$base/healthz" >/dev/null

# register
curl -sf -X POST "$base/v1/auth/register" \
  -H 'Content-Type: application/json' \
  -d '{"email":"gate@example.com","password":"correct-horse-battery"}' \
  >/dev/null || true

# login → TOKEN
TOKEN=$(curl -sf -X POST "$base/v1/auth/login" \
  -H 'Content-Type: application/json' \
  -d '{"email":"gate@example.com","password":"correct-horse-battery"}' \
  | jq -r '.token // .access_token')

test -n "$TOKEN" && test "$TOKEN" != "null"

# unauthenticated create must fail
code=$(curl -s -o /dev/null -w '%{http_code}' -X POST "$base/v1/items" \
  -H 'Content-Type: application/json' \
  -d '{"name":"nope"}')
test "$code" = "401"

# authenticated create
id=$(curl -sf -X POST "$base/v1/items" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"name":"gate-item"}' | jq -r '.id')

curl -sf -H "Authorization: Bearer $TOKEN" "$base/v1/items/$id" | grep -q gate-item
echo "OK id=$id"

Run against a live server; then restart the server and re-fetch $id to prove persistence.


Lab 3 — Verification commands

go test ./... -count=1
go test ./... -race -count=1
# if you use build tags:
go test -tags=integration ./... -count=1

go run ./cmd/service &
sleep 1
bash scripts/demo.sh
kill -TERM %1
wait || true

Self-review questions (answer in GATE-VI.md)

  1. Can a stranger run the service from README alone?
  2. Does data survive process restart?
  3. Are secrets from env (not committed)?
  4. Do 500 responses hide internals?
  5. Is every *sql query using Context?
  6. Are request bodies limited (MaxBytesReader)?
  7. Is shutdown clean (no hang)?
  8. What elective did you skip and why?

Lab 4 — Fill the evidence table

Create GATE-VI.md:

# Stage VI Gate

Date:
Module:
Go: go1.26.x

## Rubric scores
| Criterion | Score | Notes |
|-----------|-------|-------|
| Persistence |  |  |
| Auth |  |  |
| Envelope |  |  |
| Tests |  |  |
| Shutdown/slog |  |  |
| README |  |  |
| Race |  |  |
| Demo |  |  |

## Worked checklist
| Area | File(s) | Done? |
|------|---------|-------|
| Migrations | migrations/ | |
| Repo | internal/store | |
| Auth | internal/auth | |
| Errors | writeDomainErr | |
| Tests | *_test.go | |
| Shutdown | main.go | |
| README | architecture | |

## Retrospective
1. sqlc/raw/ORM choice:
2. JWT vs session pain:
3. What Stage VII needs from this service:

Lab 5 — Deliberate persistence proof

  1. Create item; note id.
  2. kill -TERM the process; confirm exit.
  3. Start again with same DSN/file.
  4. GET item by id succeeds.

If you only ever used :memory: SQLite—fail. Use a file DSN for the gate demo.

# example SQLite DSN
file:gate.db?_pragma=foreign_keys(1)

Hour-by-hour suggestion

Time Focus
0:00–0:30 Gap analysis vs gate bar
0:30–1:30 Fix persistence/auth/envelope holes
1:30–2:10 Tests + race
2:10–2:40 demo.sh + restart proof
2:40–3:00 GATE-VI.md + README architecture

Common gotchas

Gotcha Fix
In-memory only “DB” Real SQL file/engine
Auth demo-only hardcoded user Register + hashed password
README lists endpoints that 404 Re-run smoke
Integration tests not runnable Document tags / skip policy
Elective half-merge breaking build Feature flag or remove
Leaked goroutines from hub/cache Shutdown hooks
JWT secret committed Env + .env.example only
No ReadHeaderTimeout Set server timeouts
500 with raw err.Error() to clients Log server-side; generic client message

Checkpoint (gate)

  • All gate bar rows satisfied
  • Demo script succeeds against running server
  • Data persists across restart
  • Architecture paragraph + diagram present
  • go test -race green for default tests you claim
  • Rubric ≥ 80%, no zero on persistence/auth
  • Personal retrospective: Stage VI hardest topic

Commit

git add .
git commit -m "day62: stage VI gate service store auth tests"

Write three personal gotchas before continuing.


Stage VI retrospective prompts

Answer briefly in GATE-VI.md:

  1. sqlc / raw SQL / ORM—what did you choose and would you keep it?
  2. JWT vs session—what broke first?
  3. What will Stage VII (pprof, containers, metrics) need from this service?
  4. Which elective (gRPC/REST-deep, cache, WS/SSE) paid off most?

Tomorrow

Stage VII begins — Day 63 build tags & cross-compile: ship multi-arch artifacts and understand file-level build constraints—on the path from “works on my laptop” to “releasable binary.”