Day 62 — Stage VI gate
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.
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 |
Strongly recommended (bonus)
- Cache-aside on hot GET
- One realtime endpoint or documented skip with reason
- TLS lab notes linked (Day 60)
- Elective track (Days 56–57) merged or archived with reason
- Request body limits + server timeouts
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:
- Register / login
- Create an item (authenticated)
- Fetch it after process restart (persistence!)
- 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.26Checklist before demos:
- Migrations apply on empty DB.
- Register + login work.
- Create + get item works.
- Kill process; restart; get still works.
go test ./...andgo 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 || trueSelf-review questions (answer in GATE-VI.md)
- Can a stranger run the service from README alone?
- Does data survive process restart?
- Are secrets from env (not committed)?
- Do 500 responses hide internals?
- Is every
*sqlquery usingContext?
- Are request bodies limited (
MaxBytesReader)?
- Is shutdown clean (no hang)?
- 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
- Create item; note id.
kill -TERMthe process; confirm exit.
- Start again with same DSN/file.
- 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 -racegreen 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:
- sqlc / raw SQL / ORM—what did you choose and would you keep it?
- JWT vs session—what broke first?
- What will Stage VII (pprof, containers, metrics) need from this service?
- 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.”