PostgreSQL and sqlc Customer API
Build a Go customer API with sqlc, PostgreSQL tests, and query plans.
Introduction
30 Second Summary
Career changers often have working code but little evidence of how they make decisions. A reviewed project turns those decisions into proof someone else can inspect.
In this project, you will build a local customer service backed by PostgreSQL. Your handwritten SQL becomes typed Go through sqlc before the service ships as a reviewed private release.
What You'll Build
Picture showing an employer a private release that traces one working customer request from handwritten SQL to measured database evidence.
By the end of this project, you'll have:
- A local customer API where you create Ada. You can retrieve the customer through a local-only port. You can also delete the record. An unknown identifier returns 404. A duplicate email returns HTTP/1.1 409 Conflict.
- Three fresh-schema database tests that create an isolated schema for each proof. Each test applies migrations before dropping its schema.
- A reviewed private release backed by before-and-after query plans that show the list moving from Sort plus Seq Scan to Index Scan using customers_created_at_idx.
- Secret Mission: An optional interview challenge where you answer What does an index do, and when does one hurt? without Grok.
Are there any prerequisites?
You need a GitHub account that can create a private repository plus a user Project. Your Windows setup must already include Cursor with access to Grok, PowerShell 7, Go 1.27, Docker Desktop, Git, and GitHub CLI because this project does not include installation steps.
Before We Start
This is your commitment to ship one local customer service backed by PostgreSQL with evidence you can explain from design through release. The slow query plan comes before the index because its measured result gives the later change a clear reason to exist.
Set Up the Local Workspace
Your customer service needs one verified terminal lane before its design evidence can be trusted. It also needs a database that stays reachable only from your computer.
You will use Cursor as the workspace. Its built-in PowerShell 7 terminal keeps every project command in one place.
You will verify Go, Docker Desktop, Git, plus GitHub CLI. You will then start PostgreSQL with a local-only port.
In this step, get ready to:
- Verify the required command-line tools plus GitHub authentication.
- Create the pinned project configuration plus the GitHub Actions workflow.
- Start PostgreSQL 18 on 127.0.0.1:54321.
Verify tools and open the workspace
The project assumes its required tools are already installed. This check stops an outdated or missing prerequisite from causing confusing failures later.
- Press the Windows key to open Windows search.
- Type Cursor into the search field.
- Press Enter to open Cursor.
- Click Terminal in Cursor's top menu.
- Click New Terminal to open the built-in PowerShell terminal.
- Create the customers-api folder in your home directory by running this command:
mkdir ~/customers-api
What does this command do?
This creates the customers-api folder under your Windows home directory. Every local project file lives inside this folder.
- Click File in Cursor's top menu.
- Click Open Folder.
- Choose the customers-api folder from your home directory.
- Select Grok from Cursor's model selector.
- Click Terminal in Cursor's top menu.
- Click New Terminal to open a PowerShell terminal rooted at ~/customers-api.
- Check every required tool plus the current GitHub sign-in by running:
go version
docker version
git --version
gh --version
gh auth status
What should these checks prove?
- The Go output must contain go1.27.
- The Docker output must include a Server section.
- The Git plus GitHub CLI commands must print version information.
- The authentication check must confirm that GitHub CLI is signed in.
✔️ Go 1.27 and all tools respond
Your prerequisite toolchain is ready. Continue to the GitHub Project scope check.
ⓧ Go is older than 1.27
This project requires Go 1.27. An older toolchain cannot use the pinned module configuration safely.
- Pause this project while you upgrade Go through your approved software process.
- Return to Cursor after Go 1.27 is active.
- Confirm the active version by running:
go version
What should I see?
The result must contain go1.27 before you continue.
ⓧ A command is missing
The project does not include software installation. Install the missing prerequisite through your approved software process before continuing.
- Return to Cursor after the missing prerequisite is available.
- Rerun the complete prerequisite check:
go version
docker version
git --version
gh --version
gh auth status
What should I confirm?
Each command must respond successfully. Docker must also show its Server section.
GitHub Projects requires an extra authentication scope. The next command may start a browser authorization flow.
- Add the GitHub Project scope by running:
gh auth refresh -s project
What does this command do?
The command refreshes your GitHub CLI authorization with the project scope. A later phase uses this permission to create the user Project.
- Complete the browser authorization if GitHub requests it.
Your terminal now has the access needed for the private repository workflow plus its Project.
Did a prerequisite check fail?
- Start Docker Desktop from Windows search if docker version does not show a Server section.
- Complete GitHub CLI sign-in before retrying the authentication refresh.
- Confirm the terminal path points to customers-api before creating project files.
Tell me which prerequisite check failed.
Add the pinned project configuration
Pinned dependencies make the same commands reproducible on your machine plus the pull-request runner. Task runs project commands while goose controls migration state.
You will configure sqlc to generate typed Go for pgx. The workflow uses the same project definitions during review.
- Use Cursor's file sidebar to create go.mod inside customers-api.
- Pin the module plus its tool packages by pasting:
module customers
go 1.27
require (
github.com/go-task/task/v3 v3.53.1
github.com/jackc/pgx/v5 v5.11.0
github.com/pressly/goose/v3 v3.29.0
github.com/sqlc-dev/sqlc v1.31.1
)
tool (
github.com/go-task/task/v3/cmd/task
github.com/pressly/goose/v3/cmd/goose
github.com/sqlc-dev/sqlc/cmd/sqlc
)
What does this module define?
- The module name is customers.
- The Go language version is 1.27.
- The dependency block pins Task v3.53.1, pgx v5.11.0, goose v3.29.0, plus sqlc v1.31.1.
- The tool block makes Task, goose, plus sqlc available through go tool.
- Save go.mod.
- Resolve the pinned module graph by running:
go mod tidy
What does this command do?
Go resolves the pinned dependency graph. It also writes go.sum with checksums for the downloaded modules.
The first run can take a little while because Go downloads the pinned tools. You should see go.sum appear in Cursor's file sidebar when it finishes.
Did module resolution fail?
- Confirm go.mod contains every closing parenthesis shown above.
- Confirm go version reported Go 1.27.
- Retry after confirming your network can reach the Go module sources.
Help me fix my Go module setup.
- Use Cursor's file sidebar to create sqlc.yaml inside customers-api.
- Configure PostgreSQL code generation by pasting:
version: "2"
sql:
- engine: "postgresql"
schema: "sql/migrations"
queries: "sql/queries.sql"
gen:
go:
package: "db"
out: "db"
sql_package: "pgx/v5"
emit_json_tags: true
overrides:
- db_type: "pg_catalog.timestamptz"
go_type:
import: "time"
type: "Time"
What does this configuration control?
- sqlc reads migrations from sql/migrations.
- sqlc reads learner-written queries from sql/queries.sql.
- Generated Go goes into the db package.
- PostgreSQL timestamps become Go time.Time values.
- Save sqlc.yaml.
- Check Cursor's file sidebar for sqlc.yaml.
The project now has a fixed contract for generating the future db package.
Does the YAML look misaligned?
- Keep spaces in front of every nested YAML field.
- Confirm go_type sits under the timestamp override.
- Confirm the file is named sqlc.yaml.
Help me compare my sqlc configuration.
- Use Cursor's file sidebar to create .gitignore inside customers-api.
- Exclude the future recording by pasting:
talk-track.m4a
Why ignore this file?
The release recording is a generated binary asset. Git keeps the source transcript while the release stores the final audio file.
- Save .gitignore.
- Check Cursor's file sidebar for .gitignore.
The repository now has a clear rule for the future talk-track.m4a asset.
- Use Cursor's file sidebar to create Taskfile.yml inside customers-api.
- Add the database lifecycle tasks by pasting:
version: '3'
vars:
DSN: 'postgres://postgres:dev@127.0.0.1:54321/postgres?sslmode=disable'
tasks:
db:
cmds:
- docker run -d --name customers-db -e POSTGRES_PASSWORD=dev -p 127.0.0.1:54321:5432 -v customers-pgdata:/var/lib/postgresql postgres:18
ready:
cmds:
- docker exec customers-db pg_isready -U postgres -t 30
migrate:
cmds:
- go tool goose -dir sql/migrations postgres "{{.DSN}}" up
status:
cmds:
- go tool goose -dir sql/migrations postgres "{{.DSN}}" status
down:
cmds:
- go tool goose -dir sql/migrations postgres "{{.DSN}}" down
replay:
cmds:
- go tool goose -no-versioning -dir sql/migrations postgres "{{.DSN}}" up
What do these tasks control?
- The DSN points to the local PostgreSQL database.
- The db task creates the container plus named volume.
- The ready task checks PostgreSQL readiness for up to 30 seconds.
- The migration tasks apply, inspect, reverse, or deliberately replay schema files.
- Save Taskfile.yml.
- Place the generation plus test tasks directly after the replay task by pasting:
gen:
cmds:
- go tool sqlc generate
test:
cmds:
- go test ./... -count=1
run:
cmds:
- go run .
seed:
cmds:
- >-
docker exec customers-db psql -U postgres -c
"INSERT INTO customers (name, email, created_at)
SELECT 'Customer ' || g,
'c' || g || '@example.com',
now() - (g || ' minutes')::interval
FROM generate_series(1, 10000) AS g;
ANALYZE customers;"
What do these tasks prepare?
- The gen task generates typed Go from SQL.
- The test task runs the project test suite once.
- The run task starts the API.
- The seed task inserts 10000 rows before updating PostgreSQL statistics.
- Save Taskfile.yml.
- Complete the query-plan tasks directly after the seed task by pasting:
explain:
cmds:
- >-
docker exec customers-db psql -U postgres -c
"EXPLAIN SELECT * FROM customers ORDER BY created_at DESC LIMIT 20;"
explain-email:
cmds:
- >-
docker exec customers-db psql -U postgres -c
"EXPLAIN SELECT id FROM customers WHERE email = 'c42@example.com';"
What do the final tasks measure?
The explain task measures the newest-first list query. The explain-email task measures the email lookup.
- Save Taskfile.yml.
- Check that the final task name is explain-email.
The task file now covers the database lifecycle, code generation, tests, service runtime, seed data, plus both plan checks.
Is a Taskfile section misplaced?
- Keep every task aligned two spaces beneath tasks.
- Keep every cmds field aligned four spaces beneath its task.
- Confirm the file name uses the capital T shown in Taskfile.yml.
Help me find the Taskfile structure problem.
- Create a .github folder inside customers-api with Cursor's file sidebar.
- Create a workflows folder inside .github.
- Create checks.yml inside .github/workflows.
- Add the workflow trigger plus PostgreSQL service by pasting:
name: checks
on:
pull_request:
permissions:
contents: read
jobs:
checks:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:18
env:
POSTGRES_PASSWORD: dev
ports:
- 54321:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
What does this workflow section provide?
- The workflow runs for pull requests.
- The job receives read-only repository contents permission.
- The job starts PostgreSQL 18 as a service.
- The health options wait for PostgreSQL before the checks begin.
- Save .github/workflows/checks.yml.
- Complete the workflow directly after the service options by pasting:
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
- run: go build ./...
- run: go test -v -count=1 ./...
- run: go run github.com/zricethezav/gitleaks/v8@v8.30.1 git --redact
- run: go run golang.org/x/vuln/cmd/govulncheck@v1.9.0 ./...
What do the workflow steps check?
- Checkout retrieves the complete Git history for the review checks.
- Go setup reads the required version from go.mod.
- The workflow builds the service before running its real-database tests.
- Gitleaks v8.30.1 scans Git history while govulncheck v1.9.0 checks reachable Go dependencies.
- Save .github/workflows/checks.yml.
- Check Cursor's file sidebar for .github/workflows/checks.yml.
Your local configuration now defines the same build, test, secret scan, plus vulnerability scan that the pull request runs later.
Is the workflow file in the wrong place?
- Confirm checks.yml sits inside .github/workflows.
- Confirm the workflow contains one jobs section.
- Keep steps aligned with services inside the checks job.
Help me compare my pull-request workflow.
Use this checkpoint to compare every configuration file before starting the database.
✔️ Awesome, I've got everything
Great. Save every project file before continuing.
ⓧ I'd like to double check the full code
module customers
go 1.27
require (
github.com/go-task/task/v3 v3.53.1
github.com/jackc/pgx/v5 v5.11.0
github.com/pressly/goose/v3 v3.29.0
github.com/sqlc-dev/sqlc v1.31.1
)
tool (
github.com/go-task/task/v3/cmd/task
github.com/pressly/goose/v3/cmd/goose
github.com/sqlc-dev/sqlc/cmd/sqlc
)
What should match?
Your go.mod must match this complete module definition.
version: "2"
sql:
- engine: "postgresql"
schema: "sql/migrations"
queries: "sql/queries.sql"
gen:
go:
package: "db"
out: "db"
sql_package: "pgx/v5"
emit_json_tags: true
overrides:
- db_type: "pg_catalog.timestamptz"
go_type:
import: "time"
type: "Time"
What should match?
Your sqlc.yaml must match this complete generation configuration.
talk-track.m4a
What should match?
Your .gitignore must contain this single recording path.
version: '3'
vars:
DSN: 'postgres://postgres:dev@127.0.0.1:54321/postgres?sslmode=disable'
tasks:
db:
cmds:
- docker run -d --name customers-db -e POSTGRES_PASSWORD=dev -p 127.0.0.1:54321:5432 -v customers-pgdata:/var/lib/postgresql postgres:18
ready:
cmds:
- docker exec customers-db pg_isready -U postgres -t 30
migrate:
cmds:
- go tool goose -dir sql/migrations postgres "{{.DSN}}" up
status:
cmds:
- go tool goose -dir sql/migrations postgres "{{.DSN}}" status
down:
cmds:
- go tool goose -dir sql/migrations postgres "{{.DSN}}" down
replay:
cmds:
- go tool goose -no-versioning -dir sql/migrations postgres "{{.DSN}}" up
gen:
cmds:
- go tool sqlc generate
test:
cmds:
- go test ./... -count=1
run:
cmds:
- go run .
seed:
cmds:
- >-
docker exec customers-db psql -U postgres -c
"INSERT INTO customers (name, email, created_at)
SELECT 'Customer ' || g,
'c' || g || '@example.com',
now() - (g || ' minutes')::interval
FROM generate_series(1, 10000) AS g;
ANALYZE customers;"
explain:
cmds:
- >-
docker exec customers-db psql -U postgres -c
"EXPLAIN SELECT * FROM customers ORDER BY created_at DESC LIMIT 20;"
explain-email:
cmds:
- >-
docker exec customers-db psql -U postgres -c
"EXPLAIN SELECT id FROM customers WHERE email = 'c42@example.com';"
What should match?
Your Taskfile.yml must match this complete task configuration.
name: checks
on:
pull_request:
permissions:
contents: read
jobs:
checks:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:18
env:
POSTGRES_PASSWORD: dev
ports:
- 54321:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
- run: go build ./...
- run: go test -v -count=1 ./...
- run: go run github.com/zricethezav/gitleaks/v8@v8.30.1 git --redact
- run: go run golang.org/x/vuln/cmd/govulncheck@v1.9.0 ./...
What should match?
Your .github/workflows/checks.yml must match this complete pull-request workflow.
Start and verify the local database
The service needs a real PostgreSQL database for migrations plus integration tests. Docker keeps that database isolated while the explicit host address prevents access from another computer.
- Click Terminal in Cursor's top menu.
- Click New Terminal to open a second PowerShell terminal.
- Move the second terminal into the project folder by running:
cd ~/customers-api
What does this command do?
The second terminal now points to the same customers-api folder that Cursor has open.
The first image download can take a few minutes. Docker Hub reports about 154.97 MB of compressed data for the linux/amd64 image.
- Pull the official PostgreSQL 18 image by running:
docker pull postgres:18
What does this command do?
Docker downloads the PostgreSQL 18 image layers. The later database task creates a container from this local image.
You should see the pull finish successfully with the postgres:18 image available locally.
Did the image pull fail?
- Confirm Docker Desktop still shows a running Server.
- Retry after confirming your network can reach Docker Hub.
- Check that the image tag is exactly postgres:18.
Help me diagnose the PostgreSQL image pull.
- Initialize the local repository on main by running:
git init -b main
What does this command do?
Git creates the local repository metadata. It also makes main the first branch.
You should see confirmation that an empty Git repository was initialized inside customers-api.
- Create the local PostgreSQL container plus named volume by running:
go tool task db
What does the database task create?
- The container is named customers-db.
- PostgreSQL receives the local password dev.
- The named volume is customers-pgdata.
- The PostgreSQL port is published only on 127.0.0.1:54321.
Docker prints a container identifier when creation succeeds. That identifier confirms customers-db is running in the background.
Could Docker create the container?
A busy local port can block container creation. Inspect the project ports in PowerShell by running:
Get-NetTCPConnection -LocalPort 54321
Get-NetTCPConnection -LocalPort 8090
The output identifies any process already listening on the database or API port. Stop the conflicting process before retrying the database task.
Help me resolve the Docker container failure.
Before you run the final checks, do you expect the database port to appear on every network interface or only on your local host?
- Verify database readiness, the published port, plus the active branch by running:
go tool task ready
docker port customers-db
git branch --show-current
What do these checks prove?
- The readiness task asks PostgreSQL whether it can accept connections.
- The Docker port check reveals the exact host address used for PostgreSQL.
- The Git check prints the active local branch.
You should see accepting connections. You should also see a mapping containing only 127.0.0.1:54321.
The final command should print main. Your local workspace now has the required toolchain, pinned configuration, protected database port, plus pull-request checks.
Did a final check return something else?
- Wait a few seconds before retrying the readiness task if PostgreSQL is still starting.
- Confirm the port mapping contains 127.0.0.1:54321.
- Confirm the active branch is main.
Help me interpret my workspace verification output.
Your local foundation is ready. Next, you can make the service requirements, threats, tests, topology, plus trade-offs reviewable before writing application code.
Design the Delivery Plan
Your local PostgreSQL 18 database is ready. Application code can still hide weak assumptions about scope, security, testing, cost, or delivery.
A written plan makes those assumptions reviewable before the build begins. It also gives your GitHub delivery a clear definition of done.
In this step, get ready to:
- Write five design documents that define the assignment and its evidence.
- Commit the design documents before the existing configuration.
- Publish a private repository with a Project and tracked issue.
Write the design evidence
The five documents turn the project into a testable delivery plan. They define your SQL ownership before generated code or API behavior can distract from it.
- In Cursor's file sidebar, create a folder named docs inside customers-api.
- Use the new-file control beside docs to create docs/build-spec.md.
- Add the assignment and acceptance criteria by copying this first section into docs/build-spec.md:
# Build Specification
## Assignment
Ship one local Go customer service backed by PostgreSQL 18. Type every SQL line yourself. Use goose for migrations, sqlc for generated Go, pgx for database access, real PostgreSQL schemas for tests, and one reviewed pull request for delivery.
## Acceptance Criteria
- AC-1: the repository is PRIVATE.
- AC-2: the first commit is Add the design docs, with the five docs only.
- AC-3: migrate ends at version 2; status lists both files.
- AC-4: gen writes the three db files; go build ./... prints nothing.
- AC-5: go test -v -count=1 ./... shows three PASS lines and ok customers.
- AC-6: a POST answers 201; an unknown id 404; the list starts with the newest.
- AC-7: the same email again answers HTTP/1.1 409 Conflict with its detail.
- AC-8: before the index, Sort then Seq Scan; after, Index Scan using customers_created_at_idx; the email lookup uses customers_email_key.
- AC-9: replay is refused (42P07); migrate then says no migrations to run.
- AC-10: one reviewed pull request with both plans, merged; the issue Done; release v0.1.0.
- AC-11: the name Ada; DROP TABLE customers;-- with bobby@example.com answers 201 and the list still answers.
- AC-12: gh pr checks shows checks passing.
How the Specification Controls Scope
- The assignment fixes the service boundary before implementation begins.
- The acceptance criteria name the observable evidence required for delivery.
- The numbered criteria give every later test result a permanent home.
- Save docs/build-spec.md.
- Read the acceptance section in Cursor. Confirm that it ends at AC-12.
Missing an Acceptance Criterion?
Compare the criterion numbers with the reference above. Restore any missing line before you add the security section.
Ask for help if the Markdown list is broken: Help me compare my acceptance criteria with the supplied build specification.
- Place your cursor after the AC-12 line.
- Complete docs/build-spec.md by adding the security controls and scope boundary below:
## Security
| Threat | Control | Proof |
| --- | --- | --- |
| Request text changes SQL structure. | Every query uses sqlc parameters such as `$1`; no request value is joined into SQL text. | AC-11 stores SQL punctuation as the customer name and the list still answers. |
| PostgreSQL or the API is reachable from another computer. | Docker publishes `127.0.0.1:54321:5432`; the Go server binds `127.0.0.1:8090`. Never use `localhost`. | `docker port customers-db` shows only `127.0.0.1:54321`; the server log shows `127.0.0.1:8090`. |
| A key enters the repository or logs. | No API key exists. The local password is the documented value `dev`; Gitleaks runs with redaction. | Local and pull-request Gitleaks checks pass. |
| A reachable dependency has a known vulnerability. | govulncheck scans the built Go call graph in the workflow. | The pull-request check passes. |
## Out of Scope
- No screen
- No login
- No update
- No paging
- No deploy
Why These Boundaries Matter
- The security table links each threat to a control and its proof.
- The scope list prevents extra features from consuming the delivery window.
- The local-only bindings keep both service ports on your computer.
- Save docs/build-spec.md.
- Confirm that the file now contains the Security and Out of Scope headings.
Does the Security Table Look Broken?
Confirm that every table row starts and ends with a vertical bar. Keep each threat on one source line.
Use this if the table still fails to render: Help me find the Markdown table formatting problem in docs/build-spec.md.
✔️ Awesome, I've got everything!
Your build specification now fixes the assignment, evidence, security controls, and scope.
ⓧ I'd like to double check the full code
# Build Specification
## Assignment
Ship one local Go customer service backed by PostgreSQL 18. Type every SQL line yourself. Use goose for migrations, sqlc for generated Go, pgx for database access, real PostgreSQL schemas for tests, and one reviewed pull request for delivery.
## Acceptance Criteria
- AC-1: the repository is PRIVATE.
- AC-2: the first commit is Add the design docs, with the five docs only.
- AC-3: migrate ends at version 2; status lists both files.
- AC-4: gen writes the three db files; go build ./... prints nothing.
- AC-5: go test -v -count=1 ./... shows three PASS lines and ok customers.
- AC-6: a POST answers 201; an unknown id 404; the list starts with the newest.
- AC-7: the same email again answers HTTP/1.1 409 Conflict with its detail.
- AC-8: before the index, Sort then Seq Scan; after, Index Scan using customers_created_at_idx; the email lookup uses customers_email_key.
- AC-9: replay is refused (42P07); migrate then says no migrations to run.
- AC-10: one reviewed pull request with both plans, merged; the issue Done; release v0.1.0.
- AC-11: the name Ada; DROP TABLE customers;-- with bobby@example.com answers 201 and the list still answers.
- AC-12: gh pr checks shows checks passing.
## Security
| Threat | Control | Proof |
| --- | --- | --- |
| Request text changes SQL structure. | Every query uses sqlc parameters such as `$1`; no request value is joined into SQL text. | AC-11 stores SQL punctuation as the customer name and the list still answers. |
| PostgreSQL or the API is reachable from another computer. | Docker publishes `127.0.0.1:54321:5432`; the Go server binds `127.0.0.1:8090`. Never use `localhost`. | `docker port customers-db` shows only `127.0.0.1:54321`; the server log shows `127.0.0.1:8090`. |
| A key enters the repository or logs. | No API key exists. The local password is the documented value `dev`; Gitleaks runs with redaction. | Local and pull-request Gitleaks checks pass. |
| A reachable dependency has a known vulnerability. | govulncheck scans the built Go call graph in the workflow. | The pull-request check passes. |
## Out of Scope
- No screen
- No login
- No update
- No paging
- No deploy
How to Use This Reference
Compare your saved file with this complete reference. Match every heading, table row, and criterion before continuing.
- Use the new-file control beside docs to create docs/topology.md.
- Map the delivery path by copying this content into docs/topology.md:
# Topology
```mermaid
flowchart LR
Person[Career changer] --> Cursor[Cursor with Grok]
Person --> SQL[SQL written by you]
SQL --> Goose[goose migrations]
SQL --> SQLC[sqlc generation]
Goose --> PG[(PostgreSQL 18)]
SQLC --> DB[db package]
DB --> API[Go API]
API --> PG
Tests[Fresh-schema tests] --> Goose
Tests --> API
Workflow[GitHub Actions] --> Tests
PR[Reviewed pull request] --> Workflow
PR --> Release[v0.1.0]
```
The learner writes every SQL line. Grok explains the first plan and reviews the pull-request diff. Grok writes no SQL, edits no file, and runs no command. goose applies schema changes. sqlc turns the learner's queries into typed Go. Each test creates a fresh PostgreSQL schema, applies migrations, runs its proof, and drops the schema.
How the Topology Flows
The Mermaid diagram follows your SQL through migration, generation, database access, testing, review, and release.
The final paragraph fixes Grok's role. You remain responsible for every SQL line.
- Save docs/topology.md.
- Preview docs/topology.md in Cursor. Confirm that the flow starts with the career changer and ends at v0.1.0.
Does the Diagram Stay as Plain Text?
Confirm that the opening fence says mermaid. Confirm that the diagram ends with a matching closing fence.
Use this if the preview still fails: Help me diagnose why the Mermaid diagram in docs/topology.md does not render.
✔️ Awesome, I've got everything!
Your topology now shows how learner-written SQL becomes reviewed release evidence.
ⓧ I'd like to double check the full code
# Topology
```mermaid
flowchart LR
Person[Career changer] --> Cursor[Cursor with Grok]
Person --> SQL[SQL written by you]
SQL --> Goose[goose migrations]
SQL --> SQLC[sqlc generation]
Goose --> PG[(PostgreSQL 18)]
SQLC --> DB[db package]
DB --> API[Go API]
API --> PG
Tests[Fresh-schema tests] --> Goose
Tests --> API
Workflow[GitHub Actions] --> Tests
PR[Reviewed pull request] --> Workflow
PR --> Release[v0.1.0]
```
The learner writes every SQL line. Grok explains the first plan and reviews the pull-request diff. Grok writes no SQL, edits no file, and runs no command. goose applies schema changes. sqlc turns the learner's queries into typed Go. Each test creates a fresh PostgreSQL schema, applies migrations, runs its proof, and drops the schema.
How to Use This Reference
Compare your saved topology with this complete reference. Check every node, connection, and responsibility statement.
- Use the new-file control beside docs to create docs/implementation-plan.md.
- Define the phase budget by copying this content into docs/implementation-plan.md:
# Implementation Plan
| Phase | Minutes | Work |
| --- | ---: | --- |
| Setup | 9 | Verify the existing tools, create the project, pull PostgreSQL 18, and start the local-only database. |
| Design | 8 | Commit the five design docs first, commit configuration next, create the PRIVATE repository, Project, issue, and branch. |
| Build | 14 | Type the table and queries, generate Go, write the supplied service and tests, seed data, and expose the slow plan. |
| Validation | 13 | Run AC-3 and AC-5 through AC-9 and AC-11, write the index, compare plans, and record evidence. |
| Close-out | 15 | Run security checks, create and review the pull request, answer findings, merge, record the talk track, release, finish the issue, and stop PostgreSQL. |
| Total | 59 | One project. Scope unchanged. |
## FinOps
| Resource | Quantity | Rate | Total |
| --- | ---: | ---: | ---: |
| Local PostgreSQL container | 1 for an estimated 59 minutes | 0.00 USD | 0.00 USD |
| Private GitHub repository and Project | 1 each | 0.00 USD within account allowance | 0.00 USD |
| GitHub Actions runner | 1 short workflow run, estimate | 0.00 USD while included compute remains | 0.00 USD |
| Cursor with Grok | 2 short prompts | 0.00 USD while included usage remains | 0.00 USD |
| Total | | | 0.00 USD |
Do not enable paid overages. The project stays below the 5.00 USD ceiling.
How the Plan Protects Delivery
The phase table holds the unchanged project scope to 59 minutes.
The FinOps table records the cost assumptions. Included usage keeps the project at 0.00 USD.
- Save docs/implementation-plan.md.
- Confirm that the phase table totals 59 minutes.
- Confirm that the cost table ends at 0.00 USD.
Do the Tables Render Unevenly?
Keep each table row on one source line. Preserve the alignment colons in the separator rows.
Use this if a column shifts: Help me find the Markdown table problem in docs/implementation-plan.md.
✔️ Awesome, I've got everything!
Your implementation plan now makes the schedule and cost ceiling reviewable.
ⓧ I'd like to double check the full code
# Implementation Plan
| Phase | Minutes | Work |
| --- | ---: | --- |
| Setup | 9 | Verify the existing tools, create the project, pull PostgreSQL 18, and start the local-only database. |
| Design | 8 | Commit the five design docs first, commit configuration next, create the PRIVATE repository, Project, issue, and branch. |
| Build | 14 | Type the table and queries, generate Go, write the supplied service and tests, seed data, and expose the slow plan. |
| Validation | 13 | Run AC-3 and AC-5 through AC-9 and AC-11, write the index, compare plans, and record evidence. |
| Close-out | 15 | Run security checks, create and review the pull request, answer findings, merge, record the talk track, release, finish the issue, and stop PostgreSQL. |
| Total | 59 | One project. Scope unchanged. |
## FinOps
| Resource | Quantity | Rate | Total |
| --- | ---: | ---: | ---: |
| Local PostgreSQL container | 1 for an estimated 59 minutes | 0.00 USD | 0.00 USD |
| Private GitHub repository and Project | 1 each | 0.00 USD within account allowance | 0.00 USD |
| GitHub Actions runner | 1 short workflow run, estimate | 0.00 USD while included compute remains | 0.00 USD |
| Cursor with Grok | 2 short prompts | 0.00 USD while included usage remains | 0.00 USD |
| Total | | | 0.00 USD |
Do not enable paid overages. The project stays below the 5.00 USD ceiling.
How to Use This Reference
Compare your saved plan with this complete reference. Check every phase, minute value, cost assumption, and total.
- Use the new-file control beside docs to create docs/test-plan.md.
- Create the evidence ledger by copying this content into docs/test-plan.md:
# Test Plan
Paste each observed result beside its row.
| ID | Acceptance criterion | Phase | Result |
| --- | --- | --- | --- |
| AC-1 | the repository is PRIVATE. | Design | Paste `PRIVATE`. |
| AC-2 | the first commit is Add the design docs, with the five docs only. | Design | Paste the root commit message and file list. |
| AC-3 | migrate ends at version 2; status lists both files. | Validation | Paste the version and status lines. |
| AC-4 | gen writes the three db files; go build ./... prints nothing. | Build | Paste the file list and record `no output`. |
| AC-5 | go test -v -count=1 ./... shows three PASS lines and ok customers. | Validation | Paste the four lines. |
| AC-6 | a POST answers 201; an unknown id 404; the list starts with the newest. | Validation | Paste the status lines and first list item. |
| AC-7 | the same email again answers HTTP/1.1 409 Conflict with its detail. | Validation | Paste the status and detail. |
| AC-8 | before the index, Sort then Seq Scan; after, Index Scan using customers_created_at_idx; the email lookup uses customers_email_key. | Validation | Paste all three plan findings. |
| AC-9 | replay is refused (42P07); migrate then says no migrations to run. | Validation | Paste both outputs. |
| AC-10 | one reviewed pull request with both plans, merged; the issue Done; release v0.1.0. | Close-out | Paste the pull request, issue, and release URLs. |
| AC-11 | the name Ada; DROP TABLE customers;-- with bobby@example.com answers 201 and the list still answers. | Validation | Paste the status and list response. |
| AC-12 | gh pr checks shows checks passing. | Close-out | Paste the passing check line. |
How the Evidence Ledger Works
Each row pairs one acceptance criterion with the phase that proves it. The result column stores observed evidence instead of assumptions.
- Save docs/test-plan.md.
- Confirm that the table contains one result row for every criterion from AC-1 through AC-12.
Is a Result Row Missing?
Check the first column in order. Restore any skipped identifier before recording evidence later.
Use this if the sequence is unclear: Help me identify missing or duplicated rows in docs/test-plan.md.
✔️ Awesome, I've got everything!
Your test plan now has a result slot for every required proof.
ⓧ I'd like to double check the full code
# Test Plan
Paste each observed result beside its row.
| ID | Acceptance criterion | Phase | Result |
| --- | --- | --- | --- |
| AC-1 | the repository is PRIVATE. | Design | Paste `PRIVATE`. |
| AC-2 | the first commit is Add the design docs, with the five docs only. | Design | Paste the root commit message and file list. |
| AC-3 | migrate ends at version 2; status lists both files. | Validation | Paste the version and status lines. |
| AC-4 | gen writes the three db files; go build ./... prints nothing. | Build | Paste the file list and record `no output`. |
| AC-5 | go test -v -count=1 ./... shows three PASS lines and ok customers. | Validation | Paste the four lines. |
| AC-6 | a POST answers 201; an unknown id 404; the list starts with the newest. | Validation | Paste the status lines and first list item. |
| AC-7 | the same email again answers HTTP/1.1 409 Conflict with its detail. | Validation | Paste the status and detail. |
| AC-8 | before the index, Sort then Seq Scan; after, Index Scan using customers_created_at_idx; the email lookup uses customers_email_key. | Validation | Paste all three plan findings. |
| AC-9 | replay is refused (42P07); migrate then says no migrations to run. | Validation | Paste both outputs. |
| AC-10 | one reviewed pull request with both plans, merged; the issue Done; release v0.1.0. | Close-out | Paste the pull request, issue, and release URLs. |
| AC-11 | the name Ada; DROP TABLE customers;-- with bobby@example.com answers 201 and the list still answers. | Validation | Paste the status and list response. |
| AC-12 | gh pr checks shows checks passing. | Close-out | Paste the passing check line. |
How to Use This Reference
Compare your saved ledger with this complete reference. Check every identifier, criterion, phase, and result instruction.
- Use the new-file control beside docs to create docs/decisions.md.
- Add the first three decisions by copying this section into docs/decisions.md:
# Decisions
## SQL plus sqlc over an ORM
Choice: Write SQL and use sqlc to generate typed Go.
Alternative: Use an object-relational mapper.
Cost: Every query and migration is explicit and must be maintained.
Reversal trigger: Reverse when the service needs dynamic query composition that fixed SQL cannot express clearly.
## The Go is supplied and the SQL is yours
Choice: Copy the complete Go from the page, but type and explain every SQL line yourself.
Alternative: Ask Grok to write the SQL or the whole service.
Cost: More learner effort on the database boundary.
Reversal trigger: Never reverse inside this project; SQL ownership is the assignment.
## Real-database tests in fresh schemas over mocks
Choice: Each test creates `test_` plus nanoseconds, runs goose migrations into it, and drops it afterward.
Alternative: Mock generated query methods.
Cost: Tests need `customers-db` running and are slower than unit mocks.
Reversal trigger: Add mocks only for isolated application logic after real database behavior remains covered.
What the First Decisions Fix
These records preserve ownership of the database boundary. They also explain why real PostgreSQL behavior stays covered even when mocks are faster.
- Save docs/decisions.md.
- Confirm that the file contains three complete decision sections.
Is a Decision Incomplete?
Each decision needs a choice, alternative, cost, and reversal trigger. Restore the missing line before continuing.
Use this if the structure is unclear: Help me identify an incomplete decision record in docs/decisions.md.
- Place your cursor after the fresh-schema decision.
- Complete docs/decisions.md by adding the remaining decisions below:
## The index after the plan
Choice: Capture `Sort` and `Seq Scan` before writing `customers_created_at_idx`.
Alternative: Create the index with the table.
Cost: One deliberate slow-plan run and one extra migration.
Reversal trigger: Reverse only when representative production evidence shows another access path is better.
## Tools pinned in go.mod
Choice: Pin Task, goose, sqlc, and pgx in `go.mod`.
Alternative: Install untracked global versions.
Cost: `go mod tidy` and `go.sum` record a larger tool dependency graph.
Reversal trigger: Reverse when an approved dependency automation process owns upgrades and verifies compatibility.
## 127.0.0.1 ports, never localhost
Choice: Publish PostgreSQL on `127.0.0.1:54321` and bind the API to `127.0.0.1:8090`.
Alternative: Publish on every interface or use `localhost`.
Cost: The project is reachable only from this computer.
Reversal trigger: Reverse only with an authenticated deployment design, transport security, and explicit network controls.
What the Remaining Decisions Protect
The index decision preserves the planned slow result before optimization. The tool and network decisions preserve repeatability and local-only access.
- Save docs/decisions.md.
- Confirm that the complete file contains six decision headings.
Do You See Fewer Than Six Decisions?
Check that both copied sections are in the same file. Confirm that every decision heading starts with two hash characters.
Use this if a section disappeared: Help me compare docs/decisions.md with the supplied six decisions.
✔️ Awesome, I've got everything!
Your decision record now states each choice, alternative, cost, and reversal trigger.
ⓧ I'd like to double check the full code
# Decisions
## SQL plus sqlc over an ORM
Choice: Write SQL and use sqlc to generate typed Go.
Alternative: Use an object-relational mapper.
Cost: Every query and migration is explicit and must be maintained.
Reversal trigger: Reverse when the service needs dynamic query composition that fixed SQL cannot express clearly.
## The Go is supplied and the SQL is yours
Choice: Copy the complete Go from the page, but type and explain every SQL line yourself.
Alternative: Ask Grok to write the SQL or the whole service.
Cost: More learner effort on the database boundary.
Reversal trigger: Never reverse inside this project; SQL ownership is the assignment.
## Real-database tests in fresh schemas over mocks
Choice: Each test creates `test_` plus nanoseconds, runs goose migrations into it, and drops it afterward.
Alternative: Mock generated query methods.
Cost: Tests need `customers-db` running and are slower than unit mocks.
Reversal trigger: Add mocks only for isolated application logic after real database behavior remains covered.
## The index after the plan
Choice: Capture `Sort` and `Seq Scan` before writing `customers_created_at_idx`.
Alternative: Create the index with the table.
Cost: One deliberate slow-plan run and one extra migration.
Reversal trigger: Reverse only when representative production evidence shows another access path is better.
## Tools pinned in go.mod
Choice: Pin Task, goose, sqlc, and pgx in `go.mod`.
Alternative: Install untracked global versions.
Cost: `go mod tidy` and `go.sum` record a larger tool dependency graph.
Reversal trigger: Reverse when an approved dependency automation process owns upgrades and verifies compatibility.
## 127.0.0.1 ports, never localhost
Choice: Publish PostgreSQL on `127.0.0.1:54321` and bind the API to `127.0.0.1:8090`.
Alternative: Publish on every interface or use `localhost`.
Cost: The project is reachable only from this computer.
Reversal trigger: Reverse only with an authenticated deployment design, transport security, and explicit network controls.
How to Use This Reference
Compare your saved decision record with this complete reference. Check all six headings and all four fields under each heading.
Commit and publish the plan
The first Git commit becomes evidence that design led the build. Staging only the five document paths keeps the existing configuration out of that commit.
- Commit only the five design documents by running these commands in the PowerShell terminal from earlier:
git add docs/build-spec.md docs/topology.md docs/implementation-plan.md docs/test-plan.md docs/decisions.md
git commit -m "Add the design docs"
What This Commit Proves
The explicit path list stages only the design evidence. The commit message creates the required root milestone.
Your first project commit now contains the five design documents before any application code.
Did the Commit Fail?
- Confirm that all five files are saved under docs.
- Check each filename against the command before trying the same commit again.
Use this for a local Git problem: Help me diagnose why the design-doc commit failed in customers-api.
- Commit the existing project configuration separately by running these commands:
git add go.mod go.sum sqlc.yaml .gitignore Taskfile.yml .github/workflows/checks.yml
git commit -m "Add project configuration"
Why the Second Commit Is Separate
This commit records the pinned tools and pull-request workflow. Its position preserves the design-first history.
Your history now has one design commit followed by one configuration commit.
Is a Configuration File Missing?
Confirm that you are still inside customers-api. Check that the missing file exists in Cursor before repeating the command.
Use this if Git cannot stage the supplied paths: Help me diagnose a missing configuration file during the second commit.
Publishing creates a remote copy of this evidence. The repository stays private so only approved viewers can access it.
- Create the private customers-api repository by running:
gh repo create customers-api --private
What the Repository Command Does
GitHub CLI creates a remote repository named customers-api with private visibility. The next check proves that visibility directly.
Could GitHub CLI Not Create the Repository?
Confirm that your authenticated account can create private repositories. Also check whether a repository with the same name already exists.
Use this for account-specific help: Help me diagnose why GitHub CLI could not create my private customers-api repository.
Before you check, predict whether GitHub reports the new repository as public or private.
- Verify the repository visibility by running:
gh repo view customers-api --json visibility --jq .visibility
What Should You See?
You should see PRIVATE. This is the observed result for AC-1.
- Connect the local repository to the private remote by running:
git remote add origin $(gh repo view customers-api --json url --jq .url)
What the Remote Command Does
The inner GitHub CLI query reads the repository URL. Git stores that URL under the remote name origin.
- Publish the two commits to main by running:
git push -u origin main
What the Push Establishes
The push copies both commits to the private repository. The tracking link lets later pushes target origin/main automatically.
That is a key delivery milestone: your private remote now preserves the design-first commit history.
Did the Push Fail?
- Confirm that the repository creation command completed successfully.
- Confirm that GitHub CLI still reports an authenticated account.
Use this for remote or authentication help: Help me diagnose why main will not push to origin.
Create the Project work item
A GitHub Project connects the assignment to visible work tracking. The issue carries the acceptance criteria into that delivery lane.
- Create the user Project by running:
gh project create --owner "@me" --title customers-api
What the Project Represents
The Project is the work-tracking surface for this delivery. Its title matches the repository so the evidence stays easy to connect.
Could the Project Not Be Created?
Confirm that the GitHub authentication from setup still includes Project access. Check whether a user Project with the same title already exists.
Use this for scope-specific help: Help me diagnose why GitHub CLI cannot create my user Project.
- Create the delivery issue by running:
gh issue create --title "Customers backed by PostgreSQL" --body "AC-1 to AC-12 in docs/build-spec.md"
What the Issue Carries
The issue names the delivery outcome. Its body points reviewers to the complete acceptance criteria.
- Record the printed issue URL here: your printed issue URL.
- Open the printed issue URL in your browser.
- Find the Projects control on the issue page.
- Select the customers-api Project.
You should see the issue associated with customers-api. This confirms that the assignment is tracked inside the Project.
Is the Project Missing from the Issue?
Confirm that the Project belongs to the same GitHub account as the repository. Return to the Project creation output if you need to confirm the owner.
Use this if the assignment still fails: Help me add my customers-api issue to my user Project.
- Create the feature branch by running:
git switch -c customers
What the Branch Separates
The customers branch holds the service work before review. The protected evidence remains on main.
Your terminal should confirm that it switched to the new branch. Cursor's branch indicator should also show customers.
Could Git Not Create the Branch?
Check whether a local branch named customers already exists. Preserve any existing work before changing branch state.
Use this for branch help: Help me diagnose why Git cannot switch to the customers branch.
Before the final check, predict which commit should appear first in the repository history.
- Return to the private customers-api repository page.
- Open the repository commit history.
- Select the oldest commit.
- Confirm that its message is Add the design docs.
- Confirm that it contains only the five files inside docs.
The oldest commit should contain docs/build-spec.md, docs/topology.md, docs/implementation-plan.md, docs/test-plan.md, and docs/decisions.md.
- Recheck the repository visibility by running:
gh repo view customers-api --json visibility --jq .visibility
What the Final Check Proves
You should see PRIVATE again. The commit history proves AC-2.
That is the planning work locked in: your private delivery lane now starts with reviewable design evidence.
Next, you will type the database migration and queries yourself before generated Go enters the project.
Build the Customer Service
Your design now defines what the customer service must prove. The next risk sits at the database boundary because incorrect SQL can weaken every layer above it.
You will write the schema and queries yourself. Goose will apply the schema to PostgreSQL. sqlc will turn your queries into typed Go. You will finish by measuring the list plan before an index can hide its current work.
In this step, get ready to:
- Write and apply the customer table migration.
- Write four parameterized queries and generate the typed customer service.
- Seed the database and capture the pre-index list plan.
Create and migrate the customer table
A migration records a database change as a versioned file. Its upward section creates the table. Its downward section defines how to reverse the change.
- In Cursor's Explorer sidebar, create sql/migrations/00001_create_customers.sql inside ~/customers-api.
- Type every line of the migration below into sql/migrations/00001_create_customers.sql:
-- +goose Up
CREATE TABLE customers (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text NOT NULL,
email text NOT NULL CONSTRAINT customers_email_key UNIQUE,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
-- +goose Down
DROP TABLE customers;
What does this migration do?
- The identity column gives each customer a database-generated numeric identifier.
- The named email constraint prevents two customers from sharing one email address.
- The timestamp defaults record when PostgreSQL creates each row.
- The downward section removes the table when the migration is reversed.
- Save sql/migrations/00001_create_customers.sql.
- Apply migration version 1 by running:
go tool task migrate
What does this command do?
Task runs the migration command defined in Taskfile.yml. Goose reads the numbered file. PostgreSQL creates the table before Goose records version 1.
- Confirm the applied migration version by running:
go tool task status
What should you see?
The status output should show 00001_create_customers.sql as applied. This proves the running database contains the versioned customer schema.
The schema is live. Your table now enforces customer identity plus unique email behavior inside PostgreSQL.
Migration not applying?
- Confirm the file is saved at sql/migrations/00001_create_customers.sql.
- Check that both Goose annotations match the supplied migration exactly.
- Confirm the customers-db container is still running before retrying the migration.
Help me diagnose why migration version 1 is not applying to my local PostgreSQL database.
✔️ The migration is applied
Good progress. Your migration file is saved. Goose reports version 1 as applied.
ⓧ I'd like to double check the full code
-- +goose Up
CREATE TABLE customers (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text NOT NULL,
email text NOT NULL CONSTRAINT customers_email_key UNIQUE,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
-- +goose Down
DROP TABLE customers;
What to compare
Compare every column name with your file. Confirm that the downward section drops the customers table.
- Stage the migration by running:
git add sql/migrations/00001_create_customers.sql
What does this command do?
Git stages the migration as the only file in this change. This keeps the schema change separate from the service code.
- Commit the applied migration by running:
git commit -m "Add customer migration"
What does this commit capture?
The commit records your table definition as a focused database change. Its history now precedes the generated service code.
Generate and add the Go service
sqlc reads named parameterized queries. It generates methods with Go types. Request values stay separate from SQL structure because each query passes values through positional parameters.
- In Cursor's Explorer sidebar, create sql/queries.sql inside ~/customers-api.
- Type every line of the four queries below into sql/queries.sql:
-- name: CreateCustomer :one
INSERT INTO customers (name, email)
VALUES ($1, $2)
RETURNING id, name, email, created_at, updated_at;
-- name: GetCustomer :one
SELECT id, name, email, created_at, updated_at
FROM customers
WHERE id = $1;
-- name: ListCustomers :many
SELECT id, name, email, created_at, updated_at
FROM customers
ORDER BY created_at DESC
LIMIT $1;
-- name: DeleteCustomer :exec
DELETE FROM customers
WHERE id = $1;
What do these queries do?
- CreateCustomer inserts a name plus an email before returning the complete row.
- GetCustomer uses one identifier parameter to retrieve a customer.
- ListCustomers returns the newest rows first with a caller-supplied limit.
- DeleteCustomer uses one identifier parameter to remove a customer.
- Save sql/queries.sql.
- Generate the typed database package by running:
go tool task gen
What does generation create?
sqlc reads the migration to understand the table shape. It reads the four named queries to generate typed parameters plus return values.
The generated output belongs in db/db.go, db/models.go, plus db/queries.sql.go.
- Expand the db folder in Cursor's Explorer sidebar.
- Confirm the folder contains only db.go, models.go, plus queries.sql.go.
That generation step connects your SQL to compile-time Go types. You own the queries while sqlc handles the repetitive database wrapper code.
Generated files missing?
- Confirm sql/queries.sql is saved before running generation.
- Check each sqlc query annotation for the exact query name plus result mode.
- Compare the table columns with migration version 1 if generation reports an unknown column.
Help me find why sqlc did not generate the expected database package files.
✔️ I see three generated files
The generated database package is ready. Leave these three files under sqlc's control.
ⓧ I'd like to double check the generated code
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
package db
import (
"context"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
)
type DBTX interface {
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
QueryRow(context.Context, string, ...interface{}) pgx.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx pgx.Tx) *Queries {
return &Queries{db: tx}
}
What is in db.go?
The DBTX interface lets generated queries use a pool or transaction. Queries stores that database connection behind one typed API.
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
package db
import "time"
type Customer struct {
ID int64 `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
What is in models.go?
The generated Customer type mirrors the five selected database columns. Its JSON tags define the field names returned by the API.
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: queries.sql
package db
import "context"
const createCustomer = `-- name: CreateCustomer :one
INSERT INTO customers (name, email)
VALUES ($1, $2)
RETURNING id, name, email, created_at, updated_at
`
type CreateCustomerParams struct {
Name string `json:"name"`
Email string `json:"email"`
}
func (q *Queries) CreateCustomer(ctx context.Context, arg CreateCustomerParams) (Customer, error) {
row := q.db.QueryRow(ctx, createCustomer, arg.Name, arg.Email)
var i Customer
err := row.Scan(&i.ID, &i.Name, &i.Email, &i.CreatedAt, &i.UpdatedAt)
return i, err
}
const deleteCustomer = `-- name: DeleteCustomer :exec
DELETE FROM customers
WHERE id = $1
`
func (q *Queries) DeleteCustomer(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, deleteCustomer, id)
return err
}
const getCustomer = `-- name: GetCustomer :one
SELECT id, name, email, created_at, updated_at
FROM customers
WHERE id = $1
`
func (q *Queries) GetCustomer(ctx context.Context, id int64) (Customer, error) {
row := q.db.QueryRow(ctx, getCustomer, id)
var i Customer
err := row.Scan(&i.ID, &i.Name, &i.Email, &i.CreatedAt, &i.UpdatedAt)
return i, err
}
const listCustomers = `-- name: ListCustomers :many
SELECT id, name, email, created_at, updated_at
FROM customers
ORDER BY created_at DESC
LIMIT $1
`
func (q *Queries) ListCustomers(ctx context.Context, limit int32) ([]Customer, error) {
rows, err := q.db.Query(ctx, listCustomers, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Customer
for rows.Next() {
var i Customer
if err := rows.Scan(&i.ID, &i.Name, &i.Email, &i.CreatedAt, &i.UpdatedAt); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
What is in queries.sql.go?
Each named SQL query becomes a typed method on Queries. The generated methods pass values as parameters before scanning rows into Customer values.
The database package now provides typed storage operations. The supplied HTTP layer maps those operations to local routes plus safe problem responses.
- In Cursor's Explorer sidebar, create main.go inside ~/customers-api.
- Copy the complete main.go file from the reference below.
✔️ My main.go is ready
Continue when main.go matches the supplied file. Save it before moving to the tests.
ⓧ I'd like to double check the full code
package main
import (
"context"
"encoding/json"
"errors"
"log"
"net/http"
"os"
"strconv"
"customers/db"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgxpool"
)
const defaultDatabaseURL = "postgres://postgres:dev@127.0.0.1:54321/postgres?sslmode=disable"
type application struct {
queries *db.Queries
}
type problem struct {
Title string `json:"title"`
Status int `json:"status"`
Detail string `json:"detail"`
}
type createCustomerRequest struct {
Name string `json:"name"`
Email string `json:"email"`
}
func databaseURL() string {
if value := os.Getenv("DATABASE_URL"); value != "" {
return value
}
return defaultDatabaseURL
}
func newServer(queries *db.Queries) http.Handler {
app := &application{queries: queries}
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", app.health)
mux.HandleFunc("POST /customers", app.createCustomer)
mux.HandleFunc("GET /customers", app.listCustomers)
mux.HandleFunc("GET /customers/{id}", app.getCustomer)
mux.HandleFunc("DELETE /customers/{id}", app.deleteCustomer)
return mux
}
func (app *application) health(w http.ResponseWriter, _ *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
func (app *application) createCustomer(w http.ResponseWriter, r *http.Request) {
var input createCustomerRequest
if err := json.NewDecoder(r.Body).Decode(&input); err != nil || input.Name == "" || input.Email == "" {
writeProblem(w, http.StatusBadRequest, "Bad request", "name and email are required")
return
}
customer, err := app.queries.CreateCustomer(r.Context(), db.CreateCustomerParams{Name: input.Name, Email: input.Email})
if err != nil {
var pgError *pgconn.PgError
if errors.As(err, &pgError) && pgError.Code == "23505" {
writeProblem(w, http.StatusConflict, "Conflict", "a customer with this email already exists")
return
}
writeProblem(w, http.StatusInternalServerError, "Internal server error", "the database did not answer")
return
}
writeJSON(w, http.StatusCreated, customer)
}
func (app *application) listCustomers(w http.ResponseWriter, r *http.Request) {
customers, err := app.queries.ListCustomers(r.Context(), 20)
if err != nil {
writeProblem(w, http.StatusInternalServerError, "Internal server error", "the database did not answer")
return
}
writeJSON(w, http.StatusOK, customers)
}
func customerID(r *http.Request) (int64, error) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil || id < 1 {
return 0, errors.New("id must be a positive integer")
}
return id, nil
}
func (app *application) getCustomer(w http.ResponseWriter, r *http.Request) {
id, err := customerID(r)
if err != nil {
writeProblem(w, http.StatusBadRequest, "Bad request", err.Error())
return
}
customer, err := app.queries.GetCustomer(r.Context(), id)
if errors.Is(err, pgx.ErrNoRows) {
writeProblem(w, http.StatusNotFound, "Not found", "customer not found")
return
}
if err != nil {
writeProblem(w, http.StatusInternalServerError, "Internal server error", "the database did not answer")
return
}
writeJSON(w, http.StatusOK, customer)
}
func (app *application) deleteCustomer(w http.ResponseWriter, r *http.Request) {
id, err := customerID(r)
if err != nil {
writeProblem(w, http.StatusBadRequest, "Bad request", err.Error())
return
}
if err := app.queries.DeleteCustomer(r.Context(), id); err != nil {
writeProblem(w, http.StatusInternalServerError, "Internal server error", "the database did not answer")
return
}
w.WriteHeader(http.StatusNoContent)
}
func writeJSON(w http.ResponseWriter, status int, value any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(value)
}
func writeProblem(w http.ResponseWriter, status int, title, detail string) {
w.Header().Set("Content-Type", "application/problem+json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(problem{Title: title, Status: status, Detail: detail})
}
func main() {
ctx := context.Background()
pool, err := pgxpool.New(ctx, databaseURL())
if err != nil {
log.Fatalf("the database did not answer: %v; start it with: docker start customers-db", err)
}
defer pool.Close()
if err := pool.Ping(ctx); err != nil {
log.Fatalf("the database did not answer: %v; start it with: docker start customers-db", err)
}
server := &http.Server{Addr: "127.0.0.1:8090", Handler: newServer(db.New(pool))}
log.Printf("customers-api listening on http://127.0.0.1:8090")
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}
How does the server work?
The server connects through pgxpool. It binds to 127.0.0.1:8090. Its routes create, list, retrieve, plus delete customers through generated query methods.
Validation failures use structured problem responses. A duplicate email maps database code 23505 to HTTP conflict behavior.
- In Cursor's Explorer sidebar, create store_test.go inside ~/customers-api.
- Copy the complete store_test.go file from the reference below.
✔️ My store_test.go is ready
Continue when store_test.go matches the supplied file. Save it before building the service.
ⓧ I'd like to double check the full code
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"os/exec"
"strings"
"testing"
"time"
"customers/db"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgxpool"
)
func testDatabase(t *testing.T) *pgxpool.Pool {
t.Helper()
ctx := context.Background()
admin, err := pgxpool.New(ctx, databaseURL())
if err != nil {
t.Fatalf("database unavailable; start it with: docker start customers-db")
}
if err := admin.Ping(ctx); err != nil {
admin.Close()
t.Fatalf("database unavailable; start it with: docker start customers-db")
}
t.Cleanup(admin.Close)
schema := fmt.Sprintf("test_%d", time.Now().UnixNano())
if _, err := admin.Exec(ctx, "CREATE SCHEMA "+schema); err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
_, _ = admin.Exec(context.Background(), "DROP SCHEMA "+schema+" CASCADE")
})
parsed, err := url.Parse(databaseURL())
if err != nil {
t.Fatal(err)
}
query := parsed.Query()
query.Set("search_path", schema)
parsed.RawQuery = query.Encode()
schemaURL := parsed.String()
command := exec.Command("go", "tool", "goose", "-dir", "sql/migrations", "postgres", schemaURL, "up")
if output, err := command.CombinedOutput(); err != nil {
t.Fatalf("migrate test schema: %v\n%s", err, output)
}
pool, err := pgxpool.New(ctx, schemaURL)
if err != nil {
t.Fatal(err)
}
if err := pool.Ping(ctx); err != nil {
pool.Close()
t.Fatal(err)
}
t.Cleanup(pool.Close)
return pool
}
func TestCreateAndGet(t *testing.T) {
queries := db.New(testDatabase(t))
created, err := queries.CreateCustomer(context.Background(), db.CreateCustomerParams{Name: "Ada", Email: "ada-create@example.com"})
if err != nil {
t.Fatal(err)
}
got, err := queries.GetCustomer(context.Background(), created.ID)
if err != nil {
t.Fatal(err)
}
if got.Name != "Ada" || got.Email != "ada-create@example.com" {
t.Fatalf("got name=%q email=%q", got.Name, got.Email)
}
}
func TestDuplicateEmailIsRefused(t *testing.T) {
queries := db.New(testDatabase(t))
input := db.CreateCustomerParams{Name: "Ada", Email: "ada-duplicate@example.com"}
if _, err := queries.CreateCustomer(context.Background(), input); err != nil {
t.Fatal(err)
}
_, err := queries.CreateCustomer(context.Background(), input)
var pgError *pgconn.PgError
if !errors.As(err, &pgError) || pgError.Code != "23505" {
t.Fatalf("code=%v error=%v", pgError, err)
}
}
func TestDuplicateEmailIs409(t *testing.T) {
handler := newServer(db.New(testDatabase(t)))
payload := `{"name":"Ada","email":"ada-route@example.com"}`
first := httptest.NewRecorder()
firstRequest := httptest.NewRequest(http.MethodPost, "/customers", strings.NewReader(payload))
firstRequest.Header.Set("Content-Type", "application/json")
handler.ServeHTTP(first, firstRequest)
if first.Code != http.StatusCreated {
t.Fatalf("first status=%d body=%s", first.Code, first.Body.String())
}
second := httptest.NewRecorder()
secondRequest := httptest.NewRequest(http.MethodPost, "/customers", strings.NewReader(payload))
secondRequest.Header.Set("Content-Type", "application/json")
handler.ServeHTTP(second, secondRequest)
if second.Code != http.StatusConflict {
t.Fatalf("second status=%d body=%s", second.Code, second.Body.String())
}
var got problem
if err := json.NewDecoder(second.Body).Decode(&got); err != nil {
t.Fatal(err)
}
if got.Detail != "a customer with this email already exists" {
t.Fatalf("detail=%q", got.Detail)
}
}
How do the tests use PostgreSQL?
Each test creates a schema whose name begins with test_ before adding nanoseconds. Goose applies the real migrations inside that isolated schema.
Cleanup drops the schema after each test. The tests prove direct create and get behavior. They also prove database refusal plus HTTP conflict handling for a duplicate email.
- Save main.go.
- Save store_test.go.
- Compile every Go package by running:
go build ./...
What should the build show?
A successful build returns you to the terminal prompt with no output. That silence proves the server plus generated database package compile together.
- Run the three fresh-schema tests by running:
go tool task test
What do the tests prove?
The test task runs the package against the real local PostgreSQL container. Each test starts from a migrated schema instead of a database mock.
Together the tests prove create and get behavior. They also prove database-level duplicate refusal plus HTTP conflict mapping.
That is the service core working against the same database engine used by the API. Your SQL now has typed Go behavior plus real-database proof.
Build or tests failing?
- Compare main.go plus store_test.go with the supplied full-file references.
- Confirm all three generated files remain unchanged inside db.
- Check that migration version 1 is applied to the running customers-db container.
Help me diagnose a Go build or fresh-schema test failure in the customer service.
- Stage the query file plus generated package by running:
git add sql/queries.sql db main.go store_test.go
What does this command stage?
Git stages your query file with its generated package. It also stages the supplied HTTP service plus integration tests.
- Commit the working customer service by running:
git commit -m "Add generated customer service"
What does this commit capture?
The commit ties your four SQL queries to their generated Go methods. It also records the local API plus three fresh-schema tests as one working service change.
- In docs/test-plan.md, record the three generated file names beside AC-4.
- In docs/test-plan.md, record no output as the AC-4 build result.
Run the API and expose the list plan
A query can return correct rows while doing more work than expected. PostgreSQL's query planner reveals that work through named plan nodes.
- Start the local API in the terminal from earlier by running:
go tool task run
What does this command start?
The task starts the Go server. It keeps this terminal occupied while the service runs.
You should see the service listening on http://127.0.0.1:8090.
- From Cursor's top menu, select Terminal.
- Select New Terminal to keep the API running in the first terminal.
- Return the new terminal to ~/customers-api by running:
cd ~/customers-api
Why use a second terminal?
The first terminal belongs to the running API process. The second terminal lets you seed plus inspect the database without stopping that service.
- Insert the plan-check dataset by running:
go tool task seed
What does the seed task do?
The seed task inserts 10,000 synthetic customers with different timestamps. It refreshes PostgreSQL's statistics so the planner can assess the current table.
Before you inspect the plan, do you expect PostgreSQL to read rows in the required order or sort them after scanning the table?
- Reveal the current newest-first list plan by running:
go tool task explain
Read the pre-index plan
Find Sort in the observed plan. Find Seq Scan beneath it.
This is the intended shortfall. PostgreSQL currently scans the table before sorting rows for the newest-first response.
- Copy the observed Sort plan line from the terminal.
- Paste the copied plan line beside AC-8 in docs/test-plan.md.
- Copy the observed Seq Scan plan line from the terminal.
- Paste the copied plan line beside AC-8 in docs/test-plan.md.
Plan nodes look different?
PostgreSQL chooses plans from the available data plus current statistics. Confirm the seed task completed before comparing your observed plan.
Help me interpret why my pre-index list plan differs from the expected scan plus sort.
You now have a running customer API plus three real-database tests. You also have measured evidence of the list query's current cost. Next, you will validate every behavior before using that evidence to justify the index.
Validate the Database Evidence
Your customer service is running against PostgreSQL. Its recorded pre-index EXPLAIN plan exposes Sort over Seq Scan.
In this step, fresh-schema integration tests prove the database behavior. Ordered HTTP checks prove the API behavior. You then add the measured index that resolves the visible plan problem.
In this step, get ready to:
- Verify the fresh-schema integration tests.
- Prove the required HTTP behaviors.
- Measure the indexed plans plus migration safety.
Prove the API behavior
Each test creates an isolated schema whose name starts with test_. The test applies the real migrations before checking one database behavior.
- Switch to the Cursor terminal that is not running the API.
Before you run the tests, will all three fresh-schema checks pass.
- Run the integration tests with fresh results by running:
go test -v -count=1 ./...
What Does This Test Run Prove?
- The -v flag prints each test name so every proof remains visible.
- The -count=1 flag prevents a cached result from replacing a real database run.
- Each test creates its own schema before applying the migrations.
- Each test drops its schema during cleanup.
You should see three lines containing --- PASS:. The final package result contains ok customers.
Solid result. Your three checks now prove behavior against a real PostgreSQL database.
Tests Not Passing?
- Confirm the customers-db container is still running.
- Check that the test output mentions a migration or database connection before changing any code.
- Compare the failing test name with TestCreateAndGet, TestDuplicateEmailIsRefused, or TestDuplicateEmailIs409.
Help me diagnose the failing fresh-schema test.
- Paste the three passing test lines plus the final package result beside AC-5 in docs/test-plan.md.
- Save docs/test-plan.md.
The route checks now exercise success plus failure in a fixed order. This sequence proves that database constraints become safe HTTP responses.
Before you send the requests, which response do you expect when the same email is submitted twice.
- Run the ordered API acceptance checks by running these commands:
curl -s -i -X POST http://127.0.0.1:8090/customers -H 'Content-Type: application/json' -d '{"name":"Ada","email":"ada@example.com"}'
curl -s -i -X POST http://127.0.0.1:8090/customers -H 'Content-Type: application/json' -d '{"name":"Ada","email":"ada@example.com"}'
curl -s -i http://127.0.0.1:8090/customers/999999
curl -s -i http://127.0.0.1:8090/customers
What Do These Requests Prove?
- The first request proves that the create route can store a customer.
- The repeated request proves that SQLSTATE 23505 becomes HTTP/1.1 409 Conflict.
- The unknown identifier proves that a missing row becomes a safe 404 response.
- The final request proves that the list starts with the newest customer.
The first request returns 201. The repeated request returns HTTP/1.1 409 Conflict.
The unknown identifier returns 404. The list starts with the newest customer.
You now have observed route evidence for creation, conflict handling, missing rows, and newest-first ordering.
Requests Not Returning the Expected Results?
- Confirm the terminal running the API still shows the service listening on the local address.
- Check that the request body keeps the JSON quotes around each field.
- Use a different email if an earlier attempt already created the same customer.
Help me trace an unexpected customer API response.
- Paste the 201 status plus the 404 status beside AC-6 in docs/test-plan.md.
- Save docs/test-plan.md.
- Confirm the AC-6 row includes the first customer from the list.
- Paste HTTP/1.1 409 Conflict beside AC-7 in docs/test-plan.md.
- Save docs/test-plan.md.
- Confirm the AC-7 row includes a customer with this email already exists.
Add and measure the index
The recorded plan already shows the slow access path. Grok can explain that evidence under a strict boundary before you write the fix yourself.
- Return to the Grok agent in Cursor.
- Paste the recorded pre-index plan into the conversation.
- Submit this exact prompt below the plan:
Explain this PostgreSQL EXPLAIN output line by line for a beginner. Write no SQL, suggest no index, run no commands.
Why Constrain the Explanation?
The prompt gives Grok one job. It explains the measured plan while you keep ownership of the SQL choice.
The restrictions also make the result reviewable. You can count every explained line before trusting the summary.
- Check each explanation against the recorded plan.
- Record the explanation count here: number of lines Grok explained.
- Record the checked count here: number of lines you checked.
- Record the unconfirmed count here: number of lines wrong or unconfirmed.
The fix belongs in a new migration so the schema history remains ordered. goose can then apply the change as version 2.
- In Cursor's file sidebar, create sql/migrations/00002_index_customers_created_at.sql inside customers-api.
You should see the new migration listed below 00001_create_customers.sql.
- Type every line of the migration yourself using this code:
-- +goose Up
CREATE INDEX customers_created_at_idx ON customers (created_at DESC);
-- +goose Down
DROP INDEX customers_created_at_idx;
What Does This Migration Do?
- The up migration creates customers_created_at_idx on the same descending column used by ListCustomers.
- The down migration removes that index if the schema version is rolled back.
- The existing customers_email_key index still protects email uniqueness.
✔️ Awesome, I've got everything!
Your second migration contains one reversible index change.
ⓧ I'd like to double check the full code
-- +goose Up
CREATE INDEX customers_created_at_idx ON customers (created_at DESC);
-- +goose Down
DROP INDEX customers_created_at_idx;
- Save sql/migrations/00002_index_customers_created_at.sql.
Before you apply the migration, will the two query plans choose the indexes that match their access paths.
- Apply version 2 plus measure both query plans by running these commands:
go tool task migrate
go tool task status
go tool task explain
go tool task explain-email
What Do These Measurements Prove?
- The migration task applies the new schema version.
- The status task proves that both migration files are tracked.
- The list-plan task measures the newest-first query after the index exists.
- The email-plan task measures the access path created by the unique email constraint.
The migration status should reach version 2. The list evidence should contain Index Scan using customers_created_at_idx.
The email evidence should contain Index Scan using customers_email_key. These are measured results from the seeded database.
That is the key database win: the index exists because the earlier plan exposed a specific read problem.
Plan Missing the Expected Index Scan?
- Confirm the migration status lists both migration files.
- Check that created_at DESC in the migration matches the ordering in ListCustomers.
- Record the real plan output before changing the query or inventing evidence.
Help me inspect why PostgreSQL selected a different plan.
- Paste the version 2 status beside AC-3 in docs/test-plan.md.
- Save docs/test-plan.md.
- Confirm the AC-3 row lists both migration files.
- Paste the recorded before-plan nodes plus both index-plan findings beside AC-8.
- Save docs/test-plan.md.
- Confirm the AC-8 row contains all three plan findings.
The project README gathers the run instructions plus the measured proof in one place. Its three-number score makes your use of Grok auditable.
- In Cursor's file sidebar, create README.md inside customers-api.
You should see README.md at the repository root.
- Enter the full README from the reference below using your recorded counts in the three score rows:
✔️ Awesome, I've got everything!
Use the measured counts you recorded when you complete the three-number score.
ⓧ I'd like to double check the full code
# PostgreSQL and sqlc: SQL You Wrote, Go You Can Trust
## Overview
This is one career-change portfolio project. It shows design first, your SQL, generated Go, a slow plan, your index, a real test plan, one reviewed pull request, and release `v0.1.0`.
## Tools
- Cursor with Grok in Cursor
- Go 1.27
- PostgreSQL 18 in Docker Desktop
- goose, sqlc, pgx, and Task pinned in `go.mod`
- GitHub CLI and GitHub Actions
## Run
```text
docker start customers-db
go tool task ready
go tool task migrate
go tool task run
```
The API listens on `http://127.0.0.1:8090`. PostgreSQL is published only on `127.0.0.1:54321`.
## SQL ownership
You typed every line in the two migration files and the query file. Grok wrote no SQL. goose applied the migrations. sqlc generated typed Go. You wrote the index after the first plan exposed the problem.
## Grok plan prompt
Explain this PostgreSQL EXPLAIN output line by line for a beginner. Write no SQL, suggest no index, run no commands.
## Three-number score
| Measure | Number |
| --- | ---: |
| Lines Grok explained | Paste number |
| Lines you checked | Paste number |
| Lines wrong or not confirmed | Paste number |
## Proof
- `go test -v -count=1 ./...` shows three passing tests.
- Duplicate email returns `HTTP/1.1 409 Conflict`.
- SQL punctuation in a name is stored as data.
- The list plan changes from `Sort` plus `Seq Scan` to `Index Scan using customers_created_at_idx`.
- The email lookup uses `customers_email_key`.
- The reviewed pull request passes checks and is merged.
- Release `v0.1.0` contains `talk-track.m4a`.
## Interview question
What does an index do, and when does one hurt?
- Use number of lines Grok explained in the Lines Grok explained row.
- Use number of lines you checked in the Lines you checked row.
- Use number of lines wrong or unconfirmed in the Lines wrong or not confirmed row.
- Save README.md.
The saved Markdown file should show the overview plus local run commands. It should also show your completed score plus the measured proof.
README Score Still Incomplete?
- Return to the Grok response if one of the three counts is missing.
- Count the plan lines you checked against the recorded output.
- Use zero only when every explained line was confirmed.
Help me complete the three-number explanation score.
Challenge the migration and parameterization
A migration system must distinguish a tracked schema change from a forced replay. A parameterized query must also store hostile-looking text as ordinary data.
This replay check is expected to fail. The refusal proves that rerunning table creation outside normal version tracking is unsafe.
Before you run the replay, will PostgreSQL accept the original table migration again.
- Attempt the no-versioning replay by running:
go tool task replay
Why Run an Expected Failure?
The replay task ignores migration version tracking. It tries to apply the migration files in order against a schema that already contains the table.
A refusal proves that PostgreSQL protects the existing object. The failure becomes useful migration evidence.
You should see the replay refused with SQLSTATE 42P07. This is the intended negative result.
- Confirm the normal migration path remains safe by running:
go tool task migrate
What Does the Normal Migration Prove?
The normal migration path reads the tracked goose version. It recognizes that version 2 is already applied.
The result should include no migrations to run. Version tracking prevents the unsafe replay.
Before you send the final POST, will the punctuation change the SQL structure or remain customer data.
- Test parameterization plus list availability by running these commands:
curl -s -i -X POST http://127.0.0.1:8090/customers -H 'Content-Type: application/json' -d '{"name":"Ada; DROP TABLE customers;--","email":"bobby@example.com"}'
curl -s -i http://127.0.0.1:8090/customers
What Does This Safety Check Prove?
- The POST sends SQL punctuation through the parameterized create query.
- The 201 response proves that PostgreSQL stored the name as data.
- The following list response proves that the customers table still answers.
The POST returns 201. The list still returns customer data.
That result proves the request value stayed separate from the SQL command structure.
Safety Check Not Returning 201?
- Use a new email if bobby@example.com already exists from an earlier attempt.
- Keep the complete JSON body inside the single quotes.
- Confirm the list request still reaches the running local API.
Help me diagnose the parameterization check.
- Paste the replay refusal plus the normal migration result beside AC-9 in docs/test-plan.md.
- Save docs/test-plan.md.
- Confirm the AC-9 row includes SQLSTATE 42P07.
- Paste the 201 status plus the working list response beside AC-11.
- Save docs/test-plan.md.
- Confirm the AC-11 row records safe storage plus a working list.
- Stage the migration plus recorded evidence by running:
git add sql/migrations/00002_index_customers_created_at.sql README.md docs/test-plan.md
What Is Being Staged?
Git stages the index migration plus its measured evidence. The generated database package remains unchanged because the SQL query shapes did not change.
- Commit the measured index evidence by running:
git commit -m "Add measured customer index"
What Does This Commit Capture?
The commit connects one schema change to the plans that justify it. It also captures the acceptance results plus the checked Grok score.
Before the final check, do you expect the tests plus both indexed plans to remain reproducible.
- Run the end-of-step verification by running these commands:
go test -v -count=1 ./...
go tool task status
go tool task explain
go tool task explain-email
What Should the Final Check Confirm?
- The test output contains three passing test lines plus ok customers.
- The migration status reaches version 2 with both migration files.
- The list plan contains Index Scan using customers_created_at_idx.
- The email plan contains Index Scan using customers_email_key.
Your final evidence now joins passing tests, ordered HTTP behavior, migration safety, parameterization, and measured query plans.
Strong work. The database change now has a visible problem, a reversible fix, and repeatable proof.
Your database evidence is complete. Next, you will turn that evidence into a reviewed pull request plus a release.
Ship the Reviewed Release
Your PostgreSQL service now has measured plans plus acceptance evidence. The active customers branch contains the work that produced those results.
A local demo is unfinished until another reviewer can inspect its claims. This step turns your evidence into a reviewed pull request with passing checks. It closes with release v0.1.0 plus an audio handoff.
In this step, get ready to:
- Prepare the pull-request body plus the spoken delivery script.
- Answer the named review findings plus pass every automated check.
- Merge the feature branch plus publish the recorded release.
Prepare the review handoff
The docs/pr.md file connects each pull-request claim to evidence that a reviewer can inspect. Its first section defines the shipped scope plus the three measured plans.
- In Cursor's Explorer sidebar, create docs/pr.md inside the existing docs folder.
- Add the shipped scope plus the plan sections by copying this first chunk into docs/pr.md:
# Customers backed by PostgreSQL
Closes #1
## What shipped
- A local-only Go customer API backed by PostgreSQL 18.
- Three learner-written SQL files and three sqlc-generated Go files.
- Three real-database tests that use fresh schemas.
- A parameterized create path that returns 409 for duplicate email.
- A measured index for newest-first lists.
- Pull-request build, test, Gitleaks, and govulncheck checks.
## Before plan
Paste the plan containing `Sort` followed by `Seq Scan` here.
## After plan
Paste the plan containing `Index Scan using customers_created_at_idx` here.
## Email plan
Paste the plan containing `Index Scan using customers_email_key` here.
What Does This Section Capture?
- The shipped list fixes the exact scope under review.
- The plan sections preserve the evidence behind your index decision.
- The closing reference links the pull request to issue #1.
- Save docs/pr.md.
- Open Cursor's Markdown preview for docs/pr.md.
You should see the shipped scope followed by headings for the before plan, after plan, plus email plan.
Missing a Plan Section?
Check that each section heading begins with two hash characters. Confirm that docs/pr.md sits inside the docs folder.
Use this if the preview still differs from the source. Help me check the first section of docs/pr.md without changing its evidence.
The next section records how much of Grok's plan explanation you checked yourself. It also fixes the review prompt before the pull request exists.
- Add the checked score plus the named review section by copying this chunk below the email plan:
## AI score
- Lines Grok explained: paste the number.
- Lines checked: paste the number.
- Lines wrong or not confirmed: paste the number.
## Named review
Reviewer: Grok in Cursor
Prompt: Review this pull request diff as a strict senior engineer who approves a new query only with its EXPLAIN plan. List at most three findings, each one line: the file, the problem, the fix. Do not edit files and do not run commands.
### Finding 1
Finding: Paste the finding, or write `None`.
Response: Answer the finding and name the in-place fix or why no fix was needed.
Evidence: Paste the command and observed result.
Why Record the Review?
The score separates Grok's explanation from the lines you personally checked. The finding section records reviewer input plus your engineering response.
- Save docs/pr.md.
- Refresh the Markdown preview.
You should see the three-number score plus Grok in Cursor as the named reviewer.
Is the Review Section Misaligned?
Keep each score item on its own line. Keep the review prompt directly below the Prompt label.
Use this if the Markdown structure still looks wrong. Help me repair the score and review sections without changing their content.
Three finding slots keep the review bounded. Every used slot needs a response plus evidence from a command or check.
- Finish the review template by copying this chunk below the first finding:
### Finding 2
Finding: Paste the finding, or write `None`.
Response: Answer the finding and name the in-place fix or why no fix was needed.
Evidence: Paste the command and observed result.
### Finding 3
Finding: Paste the finding, or write `None`.
Response: Answer the finding and name the in-place fix or why no fix was needed.
Evidence: Paste the command and observed result.
## Acceptance
Every finding is answered. AC-1 through AC-12 have observed results in `docs/test-plan.md`. Checks pass before merge.
What Does Acceptance Mean?
Each finding receives a written response plus observed evidence. The final statement makes passing checks a condition of the merge.
- Save docs/pr.md.
- Refresh the Markdown preview.
You should see three finding sections followed by one acceptance section.
Do the Finding Numbers Skip?
Confirm that the review contains Finding 1 through Finding 3 in order. Check that the acceptance heading appears once.
Use this if a section is duplicated. Help me compare the finding headings with the required template.
Use this reference to compare the complete pull-request template before adding your recorded evidence.
✔️ Awesome, I've got everything!
Keep docs/pr.md saved before replacing its evidence placeholders.
ⓧ I'd like to double check the full code
# Customers backed by PostgreSQL
Closes #1
## What shipped
- A local-only Go customer API backed by PostgreSQL 18.
- Three learner-written SQL files and three sqlc-generated Go files.
- Three real-database tests that use fresh schemas.
- A parameterized create path that returns 409 for duplicate email.
- A measured index for newest-first lists.
- Pull-request build, test, Gitleaks, and govulncheck checks.
## Before plan
Paste the plan containing `Sort` followed by `Seq Scan` here.
## After plan
Paste the plan containing `Index Scan using customers_created_at_idx` here.
## Email plan
Paste the plan containing `Index Scan using customers_email_key` here.
## AI score
- Lines Grok explained: paste the number.
- Lines checked: paste the number.
- Lines wrong or not confirmed: paste the number.
## Named review
Reviewer: Grok in Cursor
Prompt: Review this pull request diff as a strict senior engineer who approves a new query only with its EXPLAIN plan. List at most three findings, each one line: the file, the problem, the fix. Do not edit files and do not run commands.
### Finding 1
Finding: Paste the finding, or write `None`.
Response: Answer the finding and name the in-place fix or why no fix was needed.
Evidence: Paste the command and observed result.
### Finding 2
Finding: Paste the finding, or write `None`.
Response: Answer the finding and name the in-place fix or why no fix was needed.
Evidence: Paste the command and observed result.
### Finding 3
Finding: Paste the finding, or write `None`.
Response: Answer the finding and name the in-place fix or why no fix was needed.
Evidence: Paste the command and observed result.
## Acceptance
Every finding is answered. AC-1 through AC-12 have observed results in `docs/test-plan.md`. Checks pass before merge.
- Replace the before-plan placeholder with the recorded plan containing Sort followed by Seq Scan.
- Replace the after-plan placeholder with the recorded plan containing Index Scan using customers_created_at_idx.
- Replace the email-plan placeholder with the recorded plan containing Index Scan using customers_email_key.
- Replace the three score placeholders with the checked values from README.md.
- Save docs/pr.md.
- Refresh the Markdown preview.
You should now see three measured plans plus three completed score values. Each claim has evidence beside it.
The release also needs a spoken handoff. The talk-track.md script ties your design choices to the same database evidence.
- In Cursor's Explorer sidebar, create talk-track.md at the top level of customers-api.
- Add the delivery script by copying this complete file into talk-track.md:
# Talk Track
I am a career changer. I built this as a high-performing AI full-stack engineer would ship it.
I designed first. The five design documents fixed the assignment, acceptance checks, four threats, topology, phase plan, cost, and reversal triggers before application code.
I typed every SQL line. Grok wrote no SQL. goose applied the two migrations. sqlc generated the typed Go package. The Go service used parameters instead of joining request text into SQL.
Each test created a schema named `test_` plus nanoseconds. It applied the migrations with goose, ran one proof against PostgreSQL, and dropped the schema afterward.
The first list plan showed a sort over a sequential scan. Grok explained the plan but suggested no index and ran no command. I checked the explanation, wrote `customers_created_at_idx`, migrated to version 2, and measured the second plan. The email lookup used the unique index `customers_email_key`.
I proved duplicate email returns 409. I also stored `Ada; DROP TABLE customers;--` as a normal name and listed customers afterward. This proved the request value stayed data.
Grok reviewed the pull-request diff as a named senior reviewer. I answered every finding. GitHub Actions built, tested, scanned for secrets, and checked reachable dependencies. I merged the pull request, completed the issue, and released `v0.1.0` with this recording.
The interview question is: What does an index do, and when does one hurt?
What Does the Talk Track Prove?
- The script explains why design came before implementation.
- It connects typed Go to learner-written SQL.
- It defends the index with measured plan nodes.
- It closes with review evidence plus the published release.
- Save talk-track.md.
- Open Cursor's Markdown preview for talk-track.md.
You should see the complete script ending with the index interview question.
Is the Script Incomplete?
Check that the script covers design, SQL ownership, tests, plans, parameterization, review, plus release delivery.
Use this if a paragraph is missing. Help me compare talk-track.md with the required script without rewriting it.
Use this reference to confirm the complete spoken script.
✔️ Awesome, I've got everything!
Keep talk-track.md saved for the recording after the merge.
ⓧ I'd like to double check the full code
# Talk Track
I am a career changer. I built this as a high-performing AI full-stack engineer would ship it.
I designed first. The five design documents fixed the assignment, acceptance checks, four threats, topology, phase plan, cost, and reversal triggers before application code.
I typed every SQL line. Grok wrote no SQL. goose applied the two migrations. sqlc generated the typed Go package. The Go service used parameters instead of joining request text into SQL.
Each test created a schema named `test_` plus nanoseconds. It applied the migrations with goose, ran one proof against PostgreSQL, and dropped the schema afterward.
The first list plan showed a sort over a sequential scan. Grok explained the plan but suggested no index and ran no command. I checked the explanation, wrote `customers_created_at_idx`, migrated to version 2, and measured the second plan. The email lookup used the unique index `customers_email_key`.
I proved duplicate email returns 409. I also stored `Ada; DROP TABLE customers;--` as a normal name and listed customers afterward. This proved the request value stayed data.
Grok reviewed the pull-request diff as a named senior reviewer. I answered every finding. GitHub Actions built, tested, scanned for secrets, and checked reachable dependencies. I merged the pull request, completed the issue, and released `v0.1.0` with this recording.
The interview question is: What does an index do, and when does one hurt?
Your local security checks should match the scans in GitHub Actions. Gitleaks searches the repository history for secrets. govulncheck checks the reachable Go dependency graph for known vulnerabilities.
- Run the local security checks from the existing Cursor terminal:
go run github.com/zricethezav/gitleaks/v8@v8.30.1 git --redact
go run golang.org/x/vuln/cmd/govulncheck@v1.9.0 ./...
What Do These Checks Cover?
- The first command scans Git history while redacting any detected values.
- The second command analyzes the Go call graph for reachable vulnerabilities.
Both checks should finish without a secret finding or a reachable vulnerability. This gives you local evidence before the remote workflow runs.
Did a Security Check Fail?
Read the reported file plus line before changing anything. Fix the reported cause without disabling the scan.
Use this to interpret the result safely. Help me understand this security finding without weakening the check.
- Commit the handoff files by running these commands:
git add .
git commit -m "Prepare pull request handoff"
git push -u origin customers
What Does This Commit Include?
The commit captures the pull-request body plus the spoken delivery script. The push publishes the customers branch for review.
You should see the commit complete before Git reports that the branch was pushed. The ignored audio file is not part of this commit.
Did the Push Fail?
Confirm that the terminal is still inside ~/customers-api. Check that GitHub CLI remains authenticated before retrying.
Use this for an environment-specific failure. Help me troubleshoot the customers branch push without changing repository visibility.
The pull-request command asks for a title. Use the issue title so the review evidence stays consistent.
- Create the pull request from docs/pr.md by running:
gh pr create --body-file docs/pr.md
What Does This Command Publish?
GitHub CLI reads the pull-request description from docs/pr.md. It opens the review from customers into the default branch.
- Enter Customers backed by PostgreSQL when the title prompt appears.
GitHub CLI prints the pull-request URL when creation succeeds. Keep that URL for AC-10.
Was the Pull Request Refused?
Confirm that the customers branch reached the private repository. Confirm that main remains the default branch.
Use this if GitHub CLI still cannot create the review. Help me diagnose why the pull request cannot open.
Review the feature branch
The remote workflow rebuilds the service against a fresh PostgreSQL service container. This proves the result works outside the local database state on your computer.
The named Grok review adds an engineering check before merge. You remain responsible for each response plus each necessary fix.
Before you watch the checks, do you expect the remote workflow to reproduce your local build plus test results?
- Watch the pull-request checks until they finish by running:
gh pr checks --watch
What Does the Watch Command Do?
The command stays attached to the current pull request while its checks run. It reports the final state of the checks job.
You should see the checks job pass. That result covers the build, tests, Gitleaks scan, plus govulncheck scan.
Are No Checks Visible Yet?
The workflow can take a moment to appear after pull-request creation. Run the same watch command again if the first result contains no checks.
Use this if a job fails. Help me trace the failed GitHub Actions step without removing a check.
Grok remains available in Cursor from the earlier setup. The constrained prompt requests review findings without giving the agent permission to edit files or run commands.
- Return to the Grok agent in Cursor.
- Submit this exact review prompt:
Review this pull request diff as a strict senior engineer who approves a new query only with its EXPLAIN plan. List at most three findings, each one line: the file, the problem, the fix. Do not edit files and do not run commands.
Why Constrain the Reviewer?
The prompt limits the output to review findings. It keeps every edit plus command under your control.
- Read every reported finding.
- Replace each matching Finding placeholder in docs/pr.md with Grok's exact finding.
- Enter None in each unused finding slot.
- Write your response below each finding.
- Apply each necessary fix in its named file.
- Record the proving command plus its observed result under Evidence.
Your review record should now show an answer for every finding. Each applied fix should have evidence beside it.
Before you rerun the tests, do you expect all three fresh-schema proofs to remain green after your review changes?
- Rerun the complete test suite by running:
go test -v -count=1 ./...
What Does This Rerun Protect?
The command creates fresh schemas through the existing tests. It proves that review changes preserved the database plus route behavior.
You should see three passing test lines followed by ok customers.
Did a Test Regress?
Read the first failing test before editing another file. Check whether the finding changed a query, migration, database response, or HTTP response.
Use this to isolate the regression. Help me trace the failing test to the review change.
- Record the passing checks line beside AC-12 in docs/test-plan.md.
- Record the pull-request URL beside AC-10 in docs/test-plan.md.
- Save docs/pr.md.
- Save docs/test-plan.md.
- Publish the review responses by running these commands:
git add .
git commit -m "Prepare pull request handoff"
git push -u origin customers
gh pr edit --body-file docs/pr.md
gh pr checks --watch
What Does This Update Complete?
The commit publishes your finding responses plus AC-12 evidence. The pull-request edit refreshes the remote body from the completed local file.
The final watch confirms that the updated branch still passes before merge.
You should see the checks job pass again. Your pull request now contains the measured plans plus every review response.
Did the Updated Checks Fail?
Open the failing job from the pull request. Fix the reported cause without removing a test or scan.
Use this if the second run differs from the first. Help me compare the check runs plus identify what changed.
Merge and publish the release
Passing checks make the feature branch eligible to merge. The remaining work turns the reviewed commit into a release with a recorded handoff.
The merge command removes the remote feature branch after creating the merge commit. Your local repository then returns to the updated main branch.
- Merge the reviewed pull request by running these commands:
gh pr merge --merge --delete-branch
git switch main
git pull
What Does the Merge Complete?
The first command creates the merge commit plus removes the feature branch. The remaining commands switch your local repository to the updated main branch.
You should see the pull request merge successfully. Git should report that local main is up to date after the pull.
Was the Merge Blocked?
Confirm that the latest checks passed. Resolve any reported branch conflict before retrying the merge.
Use this if GitHub still blocks the operation. Help me diagnose the blocked merge without bypassing protections.
Sound Recorder turns talk-track.md into the release asset. The recording explains the design plus the measured index trade-off in your own voice.
- Press the Windows key to open search.
- Type Sound Recorder into the search field.
- Press Enter to open Sound Recorder.
- Press Ctrl+R to start recording.
- Read talk-track.md aloud from beginning to end.
- Press Esc to stop the recording.
Keep the Evidence Audible
Pause briefly between paragraphs. Read each plan node plus index name exactly as written so the recording matches the pull-request evidence.
Finding the saved audio can be fiddly because Sound Recorder manages its own recording list. Confirm the exported filename in File Explorer before creating the release.
- Use Rename to name the recording talk-track.
- Locate the saved recording through its file controls.
- Move the recording to the top level of ~/customers-api with File Explorer.
- Confirm that the resulting filename is talk-track.m4a.
The recording is ready when File Explorer lists talk-track.m4a beside talk-track.md at the repository root.
- Publish release v0.1.0 with the recording attached by running:
gh release create v0.1.0 talk-track.m4a --notes "Customers on PostgreSQL"
What Does the Release Contain?
The command creates tag plus release v0.1.0. It uploads talk-track.m4a with the required release notes.
GitHub CLI should print the release URL. Keep it with the merged pull-request URL for AC-10.
Was the Release Asset Rejected?
Confirm that talk-track.m4a exists at the top level of ~/customers-api. Confirm that local main contains the merged work.
Use this if release creation still fails. Help me check the release tag plus local asset path.
- Return to the customers-api Project in GitHub.
- Select the Customers backed by PostgreSQL issue.
- Set its Status field to Done.
- Complete AC-10 in docs/test-plan.md with the merged pull-request URL.
- Add the issue plus release URLs to the same AC-10 result.
- Save docs/test-plan.md.
The API has served its purpose for this release. End the running process before stopping its database.
- Return to the Cursor terminal running the Go API.
- Press Ctrl+C to end the API process.
The terminal should return to its PowerShell prompt. Port 127.0.0.1:8090 is no longer serving the API.
Stopping the database preserves its container plus named volume. You can restart the same data later without rebuilding the database.
- Stop the PostgreSQL container by running:
docker stop customers-db
What Does Stopping Preserve?
Docker stops the customers-db process. The container plus customers-pgdata remain available for a later demo.
You should see customers-db printed after the stop completes.
Did the Container Refuse to Stop?
Confirm that Docker Desktop is still running. Check that the container name is exactly customers-db.
Use this to preserve your local data while troubleshooting. Help me stop customers-db without deleting customers-pgdata.
Before the final check, do you expect to see the release details plus no running customers-db container?
- Verify the release plus stopped database by running:
gh release view v0.1.0
docker ps --filter name=customers-db
What Does the Final Check Prove?
- The release command confirms that v0.1.0 exists in the private repository.
- The Docker command filters running containers for customers-db.
You should see the v0.1.0 release details. You should not see customers-db listed as a running container.
- Open the merged pull request in GitHub to confirm that its checks passed.
- Return to the Project to confirm that the issue shows Done.
- Open release v0.1.0 to confirm that talk-track.m4a appears under its assets.
That closes the delivery loop. Your reviewed main branch now has passing checks, a completed issue, plus a recorded v0.1.0 release.
Secret mission
Answer the Interview Question
Your pull request already contains the measured database evidence. With Grok closed, those plans become your only source. Your challenge is to defend the index trade-off aloud.
Clean Up Your Resources
Clean Up Your Resources
Your stopped local service has no running compute cost. Your GitHub evidence remains at 0.00 USD while usage stays within the included allowance.
Resources you used:
- A stopped PostgreSQL 18 container named customers-db, managed by Docker.
- A retained Docker volume named customers-pgdata. It holds migration version 2 with your local database data.
- The private customers-api GitHub repository. It holds the merged pull request plus the Done issue.
- The checks GitHub Actions workflow with its passing run history.
- Release v0.1.0 with its talk-track.m4a asset.
- The customers-api GitHub Project.
Keep everything running
No action needed. This option preserves your local database plus your GitHub delivery evidence.
- Keep customers-db stopped until you need another demo.
- Start customers-db for another demo by running this command:
docker start customers-db
What Does This Command Do?
Docker starts the existing customers-db container. The retained customers-pgdata volume restores migration version 2 with your customer data.
Container Not Starting?
- Confirm Docker Desktop is running.
- Help me diagnose why the customers-db Docker container will not start.
Pause - I'll come back to this later
Keep local processes stopped to free memory while retaining every project artifact for a later demo.
- Leave customers-db in its stopped state.
- Retain customers-pgdata so migration version 2 plus the local customer data remain available.
- Keep the private customers-api repository for its review evidence.
- Keep the customers-api Project for its Done issue evidence.
- Keep release v0.1.0 for its recorded talk track.
Delete - I don't want to use this again
Deletion is permanent. Take this path only when you no longer need the local database.
The Docker cleanup leaves your GitHub evidence in place until you remove it separately.
- Remove the stopped customers-db container by running this command:
docker rm customers-db
What Does This Command Do?
Docker removes only the stopped customers-db container. The customers-pgdata volume still holds your database data.
Container Removal Failed?
- Verify that customers-db remains stopped before trying the command again.
- Help me diagnose why the stopped customers-db container cannot be removed.
- Permanently delete the customers-pgdata volume by running this command:
docker volume rm customers-pgdata
What Does This Command Do?
Docker removes customers-pgdata with its local PostgreSQL data. Your local database footprint is now cleared.
Volume Removal Failed?
- Confirm the previous container-removal command succeeded.
- Help me diagnose why the customers-pgdata volume cannot be removed.
- Delete the private customers-api repository in GitHub only if you no longer need its issue, pull request, workflow history, or release evidence.
- Delete the customers-api Project in GitHub separately only if you no longer need its Done status evidence.
If you delete both GitHub resources, you should no longer see customers-api in your repository or Project lists.
Nice Work!
Nice Work!
You did it! Your private Go customer service is merged into main and published as release v0.1.0 with measured PostgreSQL evidence.
What you learned:
- Built a design-first plan before application code. It made the acceptance criteria, security controls, topology, tests, cost, and reversible decisions reviewable.
- Typed all three SQL files yourself. You used sqlc to generate trusted Go from parameterized queries. Three fresh-schema tests proved the database behavior. Your before and after plans justified customers_created_at_idx with measured evidence.
- Delivered a reviewed pull request with passing GitHub Actions checks. You merged the feature branch into main. You published release v0.1.0 with talk-track.m4a.
- Secret Mission: You answered What does an index do, and when does one hurt? without Grok. You connected Sort and Seq Scan to the later Index Scan evidence. You explained the maintenance cost for database writes.
Ready to quiz yourself?