Repository navigation
Backend skeleton: health, one catalog endpoint, OpenAPI and image #2
Description
Serve a health endpoint and one catalog endpoint from Postgres, publish the OpenAPI spec and its SDK, and ship the image the platform runs. Step 2 of the walking skeleton.
Status
| Stage | Track | Next action | Branch | PR |
|---|---|---|---|---|
| done | standard | Done | feat/2-backend-skeleton |
#3, #4 |
Appetite: 6h active · Estimate: 6h50m judgement; forecast P50 52m, P80 59m · Active: 1h 20m (from the time comment)
v0.1.0 is released and verified: ghcr.io/reference-systems-lab/commerce-backend:0.1.0@sha256:3e17e6003767524951a0626cdfd9452fcfa51f746cac9eff88cbb0ea42660505 and @reference-systems-lab/commerce-api@0.1.0. See the retro comment.
Brief
Idea: "Create the epics for the remaining repos." This is step 2 of the walking skeleton (Reference-Systems-Lab/commerce#4): the backend serves its first data from Postgres, publishes its API contract, and ships the image the platform runs.
Problem and audience: The platform runs (Reference-Systems-Lab/commerce-platform#2), but no application does. Two consumers wait on the backend:
- the storefront (step 4), which needs an endpoint and the generated SDK;
- the platform, which needs an image and a compose fragment to run.
The audience is contributors and the frontends that consume the API.
Outcome: After make up on the platform:
https://api.rsl-commerce.test/healthreports healthy;GET https://api.rsl-commerce.test/v1/productsreturns seeded products from Postgres, with prices in minor units plus a currency.
Also:
- the OpenAPI 3.1 spec is committed, and CI fails when it drifts from the code;
- the openapi-typescript and openapi-fetch SDK package is published;
- the image is on GHCR, and the backend's compose fragment declares how it runs (the platform's wiring file pins the digest).
Scope.
- In:
- a Go 1.27 module with Huma v2;
/health(liveness and readiness against Postgres);- a goose migration for a minimal
productstable, with seed rows; - a sqlc query and a paginated
GET /v1/products; - the committed spec and its drift check;
- the SDK package;
- a multi-stage build onto
distroless/static(DE-5); - the compose fragment that declares what the backend needs;
- CI: golangci-lint v2, govulncheck, tests with testcontainers-go, CodeQL
go, Dependabotgomod, and actions pinned by SHA; - the backend ADR, as the first PR.
- Out:
- auth and sessions (D7);
- the outbox and RabbitMQ;
- Valkey and Meilisearch use;
- payments and the fake gateway (D9);
- WebSocket (D7's protocol is still open);
- the billing clock beyond its port;
- every other domain and endpoint;
- the platform change that includes the fragment and routes
api., which is a separate commerce-platform sub-issue of commerce#4.
- Deferred:
GET /v1/products/{id}and the other catalog endpoints.
Constraints:
- The backend owns the API, the OpenAPI spec and the generated SDK (
README.md:11), plus the schema, migrations and seed data (README.md:21). - It declares its infrastructure, and the platform provides it (
README.md:29-30; rootAGENTS.md: repositories meet only through contracts). - Business logic never reads the system clock (
AGENTS.md:9-10). - The server decides prices (
AGENTS.md:19). - Decisions in commerce-backend#1:
- D1: Go 1.27;
- D2: code-first OpenAPI 3.1 with Huma v2, drift and breaking-change checks, and the SDK package;
- D3: pgx, sqlc and goose;
- D4: int64 minor units plus currency, and the Clock port;
- D6: the CI tools and
distroless/static-debian13:nonroot; - D8:
CACHE_URLfor Valkey.
- Also Choose the tech stacks commerce#3 DE-4 (CI security), DE-5 (containers) and DE-7 (the SDK), and commerce-platform#1 P-D1 (per-app
compose.platform.yaml, digest-pinned GHCR images, opt-in local builds).
Known in code:
- The repo is docs-only:
README.md:46-48says "Planning. No code yet." .githooks/setupholds the git-secrets hooks.- The platform answers 503 for
api.today (platform/proxy/conf.d/90-not-running.conf:6), and itscompose.yamlincludes no app fragment.
Track: standard: the spike settled every outside question; what's left is answerable from the code and the decisions. · Appetite: 6h active
| ID | Research question | Track | Why it matters to the plan |
|---|---|---|---|
| RQ-1 | What does the fragment contract look like? The service name, the network (data plus edge, D-13), the healthcheck with no shell in a distroless image, how secrets are passed (file-sourced, P-D5), and how the image digest gets bumped. |
code | It fixes the interface the platform sub-issue implements |
| RQ-2 | Where does the SDK package live and publish to (GitHub Packages under @reference-systems-lab, like DE-10), and who generates it: backend CI on release, or the consumer? |
code | It decides the release job and the storefront's install path |
Assumptions:
- The SDK is published privately to GitHub Packages under
@reference-systems-lab, as the design-system packages are (DE-10). To be confirmed in the plan. - The seed data loads through a migration or a
seedcommand. The platform'smake seedarrives with this step or the platform sub-issue. - The catalog is one
GET /v1/productsreading seeded rows (@ABilenduke, 2026-10-08).
Revisions
- 2026-10-08 (plan): the image digest is pinned in the platform's wiring file, not in the backend's fragment, per commerce-platform#1 P-D2 (D-6). The Outcome line was corrected in place. RQ-1 is answered by D-6 and D-7, and RQ-2 by D-1.
Spec
| ID | Requirement (source) | Priority | Acceptance |
|---|---|---|---|
| REQ-001 | One binary with subcommands (#1 research, Recommendation; commerce-platform#1 P-D2 follow-ups) | Must | The backend shall provide backend serve, migrate, seed, healthcheck and openapi. If the subcommand is unknown or missing, then it shall print usage and exit 2. If a required environment variable is missing, then serve, migrate and seed shall exit non-zero with a message naming the variable. |
| REQ-002 | Configuration from the environment and files (commerce-platform#1 P-D5; commerce-platform#2 D-5) | Must | The backend shall read DATABASE_URL, which carries no password, and the password from the file named by DATABASE_PASSWORD_FILE. PORT defaults to 8080. If the password file is missing or empty, then it shall exit non-zero, naming the variable but not the value. The password shall never appear in logs, errors or the spec. |
| REQ-003 | Health (brief) | Must | While Postgres answers a ping within 2 s, GET /health shall return 200 {"status":"ok"}. If it doesn't, then GET /health shall return 503 {"status":"unavailable"}. When backend healthcheck runs, it shall call http://127.0.0.1:$PORT/health and exit 0 on 200, or 1 otherwise or after a 3 s timeout, with no shell. |
| REQ-004 | Schema and migrations (#1 D3; README.md:21) |
Must | When backend migrate runs, it shall apply the goose migrations embedded in the binary, and a second run shall apply nothing and exit 0. The products table has: • id bigint identity, the primary key; • slug text, unique; • name text, not null; • price_amount bigint, >= 0; • price_currency char(3), matching ^[A-Z]{3}$; • created_at timestamptz. |
| REQ-005 | Seed (brief; commerce-platform#1 FA60) | Must | When backend seed runs, it shall upsert 6 fixed products by slug. A second run shall leave the row count and every value unchanged. |
| REQ-006 | Catalog list (brief; #1 D4) | Must | When a client sends GET /v1/products?limit=&cursor=, the backend shall return 200 with {"items":[{"id","slug","name","price":{"amount","currency"}}],"next_cursor"}. • Items are ordered by id. • amount is in minor units. • limit is 1–100 and defaults to 20. • next_cursor is null on the last page. Given the 6 seeded products and limit=4, following next_cursor shall return each product exactly once, over 2 pages. If limit is outside 1–100 or cursor is malformed, then it shall return 400 as application/problem+json, with no SQL or internals in the body. |
| REQ-007 | HTTP hygiene (commerce#3 DE-5; commerce-platform#1 §3 headers) | Must | Every response shall carry X-Content-Type-Options: nosniff and Cache-Control: no-store, and never Strict-Transport-Security (the proxy owns it). The server shall set non-zero read-header, read, write and idle timeouts. When it receives SIGTERM, it shall stop accepting connections and exit 0 once in-flight requests finish, within 10 s. |
| REQ-008 | OpenAPI contract (#1 D2) | Must | When backend openapi runs, it shall print the OpenAPI 3.1 document without a database. The document is committed as api/openapi.json. If the committed file differs from the generated one, then CI shall fail. If oasdiff breaking against the base branch's api/openapi.json reports an error-level change, then CI shall fail. The check is skipped when the base branch has no spec. |
| REQ-009 | Boundaries and clock (AGENTS.md:8-10; #1 D4, D6) |
Must | If code in internal/catalog imports a domain package other than internal/platform/..., then go-arch-lint shall fail. If code outside internal/platform/clock calls time.Now, then golangci-lint (forbidigo) shall fail. |
| REQ-010 | Image (#1 D6; commerce#3 DE-5) | Must | The Dockerfile shall build onto gcr.io/distroless/static-debian13:nonroot, with every FROM pinned by digest. The image: • runs as 65532:65532; • uses an exec-form ENTRYPOINT ["/backend"]; • sets HEALTHCHECK CMD ["/backend","healthcheck"] and the org.opencontainers.image.source label; • builds for linux/amd64 and linux/arm64. |
| REQ-011 | Compose fragment (commerce-platform#1 P-D2; DE-5) | Must | compose.platform.yaml shall define only backend-migrate (runs migrate and exits) and backend-api (runs serve). Each service: • sets no image:, ports, networks or depends_on, and names no infrastructure host; • takes every value the platform provides as ${VAR:?} (the secret's path, /run/secrets/postgres_password, is fixed by Compose); • is read_only with cap_drop: [ALL], no-new-privileges, init and a sized /tmp tmpfs; • uses the secret postgres_password. backend-api also has the healthcheck. When CI includes the fragment from a stub platform file (Postgres plus a stub wiring file with a locally built image), backend-migrate shall exit 0, backend-api shall become healthy, and /v1/products shall return the seeded rows after seed. |
| REQ-012 | SDK package (#1 D2; commerce#3 DE-7, DE-10) | Must | sdk/ shall build @reference-systems-lab/commerce-api from api/openapi.json: • openapi-typescript paths types; • a typed openapi-fetch createClient; • ESM only; • the DE-2 hardened .npmrc; • publishConfig.registry set to https://npm.pkg.github.com. On every PR, CI shall type-check a usage sample against the freshly generated types. A wrong field name in the sample shall fail it. |
| REQ-013 | CI and updates (commerce#3 DE-4; #1 D6) | Must | On every PR, CI shall pass with contents: read, persist-credentials: false, a timeout per job and a concurrency group. It runs: • golangci-lint v2; • go-arch-lint; • govulncheck; • go test -race, with testcontainers-go on the platform's postgres:18.6-trixie digest; • sqlc diff; • the two spec checks; • hadolint; • the fragment job; • the SDK type-check. Every uses: is a 40-hex SHA with a # vX.Y.Z comment. CodeQL's matrix adds go. Dependabot covers gomod, docker, github-actions and npm (/sdk), with groups and a 7-day cooldown. |
| REQ-014 | Release (@ABilenduke, 2026-10-08; DE-4) | Must | When a GitHub release vX.Y.Z is published, the release workflow shall: • push ghcr.io/reference-systems-lab/commerce-backend:X.Y.Z for amd64 and arm64, with an SBOM, provenance and an actions/attest build-provenance attestation; • run Grype on the image and fail on a critical finding; • attach openapi.json to the release; • publish the SDK at X.Y.Z to GitHub Packages, with an attested tarball. Only the publishing jobs hold write scopes, and the SDK is built without them. If the tag isn't on a commit on main, then nothing shall publish. If a step fails, then nothing after it shall publish. If a version is already published, then a rerun shall not push it again. |
| REQ-015 | v0.1.0 (@ABilenduke, 2026-10-08) | Must | When the code PR has merged and @ABilenduke approves, v0.1.0 shall be released. Then: • an anonymous docker pull ghcr.io/reference-systems-lab/commerce-backend@<digest> succeeds; • gh attestation verify --owner Reference-Systems-Lab passes for the image and the SDK tarball; • npm install @reference-systems-lab/commerce-api@0.1.0 with a read:packages token type-checks the sample. |
| REQ-016 | Docs (root AGENTS.md ADRs; commerce#4) |
Must | docs/adr/0001-backend-stack.md shall record #1 D1–D9 and this slice's decisions D-1 to D-9, landing first in its own PR (Part of #2). README.md shall cover running it under the platform, the subcommands, the environment, releasing and the SDK. |
Quality requirements:
- Security: no secret in images, logs, errors or
api/openapi.json; distroless with no shell. - Reliability:
migrate,seedand the release workflow are idempotent. - Hypotheses, measured and not gates: the compressed image is under 30 MB (the spike measured 4.9 MB, F100), and
GET /v1/productsanswers in under 50 ms locally.
Non-goals:
- auth, sessions and CORS (no browser calls the API yet; the storefront renders on the server);
- the outbox, RabbitMQ, Valkey and Meilisearch;
- payments and the fake gateway (Choose the backend stack #1 D9);
- WebSocket (Choose the backend stack #1 D7 is still open);
- OpenTelemetry (it arrives with the platform's
observabilityprofile); GET /v1/products/{id};- rate limiting;
- the platform wiring, routing and
make seed(Run applications from their compose fragments commerce-platform#7); - making the new CI jobs required checks (a follow-up, with the user's OK).
Plan
Approach
The repo is docs-only at 38a8548:
README.mdandAGENTS.md;.github/workflows/codeql.yml:24analyzes onlyactions, and pins actions by tag (:27,30,36);.github/dependabot.yml:3-8covers only github-actions;.githooks/setup:14adds the git-secrets patterns;.gitignoreignores only.agent/.
Everything below is proposed.
Binary. A single static backend binary built from cmd/backend. Its subcommands are a stdlib switch (D-4): serve, migrate, seed, healthcheck and openapi.
Layers (enforced by .go-arch-lint.yml):
internal/platform/{config,db,clock,httpserver}: shared infrastructure that domains may use;internal/catalog/{store,service,http}: the first domain, which may import onlyplatform;internal/health.
clock defines the Clock port with a system implementation. forbidigo bans time.Now elsewhere (#1 D4).
HTTP.
- Huma v2 on the Go 1.22+
net/httpmux (humago adapter). Huma validates parameters and writes RFC 9457 problem details. - A middleware adds
nosniffandno-store. httpserversets the timeouts and drains on SIGTERM.- Logs are slog JSON, with the database password never in them, since it is read from a file into
pgxpool.Configand never into a URL (D-7).
Data.
- pgx v5 pool.
- goose migrations embedded with
embed(db/migrations/00001_products.sql). - sqlc generates
internal/catalog/storefromdb/queries/catalog.sql, andsqlc diffin CI catches stale code. - The keyset cursor is base64url of the last
id, validated on decode (D-8). seedusesINSERT … ON CONFLICT (slug) DO UPDATEfor 6 fixed products.
Contract.
backend openapimarshals Huma'sOpenAPI()(3.1), soapi/openapi.jsonis generated, never hand-edited.- CI regenerates it and runs
git diff --exit-code. oasdiff breaking(a pinned image) compares it with the base branch's copy.
SDK (sdk/, D-1):
npm run generateruns openapi-typescript against../api/openapi.jsonand producesschema.d.ts;index.jsandindex.d.tsexportcreateClientfrom openapi-fetch, typed withpaths;- the version is set from the release tag;
- the DE-2
.npmrc; Node 24 (26 from 2026-10-28).
Container and fragment.
- Dockerfile:
- the first stage is
golang:1.27.x-trixie@sha256on$BUILDPLATFORM, cross-compiling withGOOS/GOARCHfromTARGETARCH,CGO_ENABLED=0,-trimpathand-ldflags=-s -w(D-9); - the final stage is
distroless/static-debian13:nonroot@sha256.
- the first stage is
compose.platform.yaml:backend-migrate(commandmigrate,restart: "no") andbackend-api(commandserve, plus the healthcheck);DATABASE_URL: ${BACKEND_DATABASE_URL:?},DATABASE_PASSWORD_FILE: /run/secrets/postgres_password,secrets: [postgres_password], and the hardening;- no
image:, networks ordepends_on(D-6). The platform's wiring file (Run applications from their compose fragments commerce-platform#7) adds them, withbackend-apidepending onbackend-migrate: service_completed_successfully(D-5).
ci/compose.stub.yaml: the platform's Postgres digest, the secret from a temp file, and aninclude:of[../compose.platform.yaml, ci/compose.wiring.yaml]. The wiring file uses a local build, which proves the merge the platform relies on (commerce-platform#1 FA8, FA20).
CI (.github/workflows/ci.yml):
lint: golangci-lint, go-arch-lint,sqlc diff, the spec drift and oasdiff checks, hadolint, and the SHA-pin check.test:go test -race ./...with testcontainers-go.vuln: govulncheck.fragment: the stub stack, withup --wait, curls, adocker inspecthardening check anddown -valways.sdk:npm ci, generate, type-check.
Every action is SHA-pinned. CodeQL is re-pinned and adds go.
Release (.github/workflows/release.yml, on release: published, D-2):
image: buildx for amd64 and arm64 with--sbomand--provenance, a push to GHCR, Grype failing on critical, andactions/attest-build-provenanceon the digest. It needspackages: write,id-token: writeandattestations: write.assets: uploadsopenapi.jsonwithcontents: write.sdk: depends onimage. It sets the version from the tag, runsnpm pack, attests the tarball, thennpm publishwithpackages: write.
Alternatives not chosen:
- humacli or cobra (more dependencies);
- running migrations at
servestart (it races between replicas, and P-D2 asks for separate commands); - offset pagination (it drifts under inserts);
- publishing the SDK from the storefront (DE-7 says the backend publishes it);
- release-please (the user chose manual releases).
Steps
- S1: Backend ADR (REQ-016).
docs/adr/0001-backend-stack.md: Choose the backend stack #1 D1–D9, and D-1 to D-9 below, in the format of the platform's ADR 0001.- Its own PR, "Part of Backend skeleton: health, one catalog endpoint, OpenAPI and image #2"; @ABilenduke merges.
- Check: inspection against Choose the backend stack #1 Decisions.
- Estimate 30m · Basis: P50 3m, P80 5m (build, 10 past steps)
- S2: Binary, config and health (REQ-001, REQ-002, REQ-003, REQ-007).
go.mod,cmd/backend,internal/platform/*,internal/health, and the Huma server.- Check:
- unit tests for usage and exit 2, missing variables, the password redacted from errors and logs, and the headers;
- testcontainers tests for
/health200, then 503 after the container stops; healthcheckexit codes;- a SIGTERM drain test with a slow handler.
- Estimate 45m · Basis: P50 5m, P80 8m (build, 10)
- S3: Schema, seed and catalog (REQ-004, REQ-005, REQ-006). Depends on S2.
- The migration, sqlc and the store; the catalog service and handler;
seed. - Check, as testcontainers integration tests:
- migrate twice, then
goose status; - seed twice gives the same count and a hash of the rows;
- with
limit=4, 2 pages return 6 unique ids; limit=0,limit=101andcursor=%%%return 400 problem+json, with nopq/SQLtext;- inserting a negative price or the currency
usdviolates a constraint.
- migrate twice, then
- Estimate 1h · Basis: P50 6m, P80 11m (build, 10)
- The migration, sqlc and the store; the catalog service and handler;
- S4: OpenAPI contract (REQ-008). Depends on S3.
backend openapi, the committedapi/openapi.json,make specandmake check-spec(proposed).- Check: editing a summary without regenerating fails drift; renaming
next_cursorfails oasdiff against a saved copy. - Estimate 30m · Basis: P50 3m, P80 5m (build, 10)
- S5: Image and fragment (REQ-010, REQ-011). Depends on S3.
Dockerfile,compose.platform.yaml,ci/compose.stub.yamlandci/compose.wiring.yaml.- Check:
docker buildx build --platform linux/amd64,linux/arm64succeeds;- the stub stack comes up with
--wait, thenrun --rm backend-api seed, and curl gets/health200 and the 6 products; docker inspectshows the user, read-only root, capabilities and no ports;- the image size is recorded.
- Estimate 45m · Basis: P50 5m, P80 8m (build, 10)
- S6: Lint, boundaries and CI (REQ-009, REQ-013). Depends on S2–S5.
.golangci.yml(forbidigo, gosec, depguard),.go-arch-lint.yml,ci.yml, thecodeql.ymlre-pin plusgo, anddependabot.yml.- Check:
- a planted
time.Nowincatalogfails; - a planted
catalog→healthimport fails; - every
uses:matches@[0-9a-f]{40}; - the first CI run on the PR is green.
- a planted
- Estimate 45m · Basis: P50 5m, P80 8m (build, 10)
- S7: SDK package (REQ-012). Depends on S4. Independent of S5 and S6.
sdk/package.json,.npmrc, the generate script,index.jsandindex.d.ts,sample.tsandtsconfig.json.- Check: the sample type-checks, and changing
itemstoitemin it failstsc. - Estimate 30m · Basis: P50 3m, P80 5m (build, 10)
- S8: Release workflow (REQ-014). Depends on S5 and S7.
release.yml.- Check: actionlint and the pin check pass; the permissions are reviewed per job. The real run is S10.
- Estimate 45m · Basis: P50 5m, P80 8m (build, 10)
- S9: README (REQ-016).
- Replace "Planning. No code yet." with how to run it, the commands, the environment, releasing and the SDK.
- Check: inspection.
- Estimate 15m · Basis: P50 2m, P80 3m (build, 10)
- qa: Adversarial review and rehearsal (all REQs).
- Independent read-only reviewers for security, containers and compose, Go correctness, and acceptance coverage. Then a rehearsal from a clean clone:
maketargets, the stub stack, and the SDK (the rule proposed in commerce-platform#6). - Check: each finding is fixed with its own commit or recorded.
- Estimate 45m · Basis: P50 5m, P80 10m (all, 19)
- Independent read-only reviewers for security, containers and compose, Go correctness, and acceptance coverage. Then a rehearsal from a clean clone:
- S10: v0.1.0 (REQ-015). After the code PR merges, and only with @ABilenduke's go-ahead.
gh release create v0.1.0, watchrelease.yml, then:- make the GHCR package public if an anonymous pull fails;
gh attestation verifyfor the image and the SDK;- install the SDK with a
read:packagestoken and type-check the sample; - grant
commerce-storefrontRead on the SDK package.
- Estimate 20m · Basis: P50 2m, P80 4m (build, 10)
Total: judgement 6h50m. The forecast gives P50 52m and P80 59m (worklog forecast --repo Reference-Systems-Lab/commerce-platform, per commerce#4 D-1; 10 past build steps, all from one issue). With QA and fixes the total is unchanged (+0% over 3 issues). Appetite: 6h. If time passes the appetite, the cuts are, in order: arm64, the oasdiff check, then the Grype gate.
Tests
| Requirement | Test or check (planned) |
|---|---|
| REQ-001 | Unit: the subcommand table, usage and exit 2, missing-variable messages |
| REQ-002 | Unit: password from the file, missing or empty file, redaction in errors and logs; grep the spec for the value |
| REQ-003 | testcontainers: 200, then 503 after Stop; the healthcheck exit codes against an httptest server and a closed port |
| REQ-004 | testcontainers: migrate twice, goose status, constraint violations |
| REQ-005 | testcontainers: seed twice, the same count and row hash |
| REQ-006 | testcontainers plus httptest: the pages, the cursor walk, 400s, problem+json content type, no internals |
| REQ-007 | Unit: headers on 200, 400 and 503; server timeouts set; a SIGTERM drain with a 1 s handler |
| REQ-008 | CI: drift check and oasdiff; locally, a planted drift and a planted breaking change |
| REQ-009 | CI: go-arch-lint and golangci-lint; locally, planted violations |
| REQ-010 | buildx for two platforms; docker inspect (user, entrypoint, healthcheck, label); hadolint |
| REQ-011 | CI fragment job: up --wait, seed, curls, the hardening inspect, down -v |
| REQ-012 | CI sdk job: generate and type-check the sample; locally, a planted wrong field |
| REQ-013 | The PR's checks all pass; the pin grep |
| REQ-014 | actionlint; the v0.1.0 run in S10 |
| REQ-015 | S10: anonymous pull, gh attestation verify for both artifacts, SDK install and type-check |
| REQ-016 | Inspection |
Risks
| Risk (pre-mortem: "it failed because…") | Likelihood | Impact | Mitigation |
|---|---|---|---|
The fragment's secrets: [postgres_password] doesn't resolve under include, because the secret is defined in the platform's file |
Medium | Medium | The stub CI job proves it in S5. If it fails, the fragment declares the secret and the wiring file sets its file: (recorded for commerce-platform#7) |
| GHCR creates the package as private, so the platform can't pull anonymously | High | Medium | S10 tests an anonymous pull; the user flips visibility once (P-D2 wants public images) |
| The first PR has no base spec, so oasdiff errors | High | Low | Skip when the base has no api/openapi.json |
| testcontainers is slow or flaky in CI | Low | Low | One Postgres container per package, pinned by digest, with job timeouts |
| The arm64 build breaks | Low | Low | Pure-Go cross-compile with no QEMU; the first cut if it costs time |
| Information disclosure: errors leak SQL or the password | Low | Medium | Problem details only, password redaction tests, and a grep of the spec |
Tampering or denial of service: cursor or limit abuse |
Low | Low | Opaque, validated cursors, limit ≤ 100, server timeouts |
| Elevation through the release workflow's write tokens | Low | High | Write scopes only on the jobs that publish, SHA-pinned actions, no pull_request_target, triggered only by a published release |
Rollout and rollback
Nothing consumes the API until commerce-platform#7 wires it in. In the repo, rollback means reverting the PR. A bad release is fixed by releasing a new version, never by moving a tag. The platform keeps its previous digest pin until it is bumped.
Definition of Ready
- Every Must has acceptance and a step that names it
- No blocking decision is Open
- Every step has an estimate and a basis, and the total fits the appetite (P80 59m against 6h)
- Approved by @ABilenduke on 2026-10-08
Definition of Done
- Every in-scope acceptance check passes and is in the journal (S1–S10, qa;
journal:summary) - The repository's required checks pass (all CI and CodeQL jobs on Backend skeleton: health, one catalog endpoint, OpenAPI, SDK and image #4 and on
mainat929b6f1) - The PR carrying
Closes #2is merged (Backend skeleton: health, one catalog endpoint, OpenAPI, SDK and image #4,929b6f1), and v0.1.0 is released and verified (S10) - ADRs and docs the change needs are updated (ADR 0001 in Record the backend stack #3,
8a0332f; README) - Follow-ups are open issues, listed under Links (none required; optional ones are proposed in the retro, awaiting @ABilenduke)
- Time posted and the retro written
Revisions
- 2026-10-08 (qa): two acceptance criteria were reworded, with the same IDs and no scope change.
- REQ-011: "every environment value as
${VAR:?}" now reads "every value the platform provides". The plan itself prescribes the literalDATABASE_PASSWORD_FILE: /run/secrets/postgres_password, which Compose fixes. - REQ-014: "a job that alone holds
packages: write" was impossible, because pushing to GHCR also needs it. It now reads that only the publishing jobs hold write scopes and the SDK is built without them. The criterion also gains three checks from the review: the tag must point at a commit onmain, each stage stops everything after it, and a rerun never republishes a version.
- REQ-011: "every environment value as
Decisions
| ID | Decision | Kind | Source |
|---|---|---|---|
| D-1 | The SDK is @reference-systems-lab/commerce-api, published privately to GitHub Packages, with consumer repos granted Read through "Manage Actions access" (as commerce#3 DE-10). Answers RQ-2. |
Confirmed | @ABilenduke, 2026-10-08 |
| D-2 | Releases are manual GitHub releases vX.Y.Z. One workflow publishes the GHCR image, openapi.json and the SDK at that version. |
Confirmed | @ABilenduke, 2026-10-08 |
| D-3 | v0.1.0 is cut in this issue (S10), after the code PR merges and with @ABilenduke's go-ahead. | Confirmed | @ABilenduke, 2026-10-08 |
| D-4 | Subcommands are a stdlib switch, not humacli or cobra, for a smaller supply chain. | Default | Plan; #1 research F96 |
| D-5 | migrate runs as a one-shot backend-migrate service. The platform makes backend-api wait for it (service_completed_successfully). seed runs through make seed (run --rm backend-api seed). |
Default | Plan; commerce-platform#1 §2 |
| D-6 | The fragment sets no image:, networks or depends_on. The platform's wiring file compose/compose.backend.yaml holds the digest pin and the wiring. Answers RQ-1, and corrects the brief, which put the pin in the fragment. |
Confirmed | commerce-platform#1 P-D2 |
| D-7 | The Postgres password reaches the backend only as the postgres_password secret, via DATABASE_PASSWORD_FILE. It never appears in a URL or the environment. DATABASE_URL comes from the platform as ${BACKEND_DATABASE_URL:?}. |
Default | Plan; commerce-platform#2 D-5 |
| D-8 | Money in JSON is {amount: int64 minor units, currency: ISO 4217}. The list uses keyset pagination with an opaque base64url cursor. |
Default | Plan; #1 D4 |
| D-9 | The image is built for linux/amd64 and linux/arm64 by Go cross-compilation on $BUILDPLATFORM, with no QEMU, so macOS contributors on Apple silicon run it natively. |
Default | Plan; commerce-platform#1 P-D0b |
| D-10 | The plan is approved. | Confirmed | @ABilenduke, 2026-10-08 ("I approve the plan") |
ADR: docs/adr/0001-backend-stack.md, written in S1.
Links
- Epic: Walking skeleton commerce#4 (Walking skeleton, step 2)
- Decisions this builds on: Choose the backend stack #1 (backend stack, D1–D9), Choose the tech stacks commerce#3 (DE-4, DE-5, DE-7), Choose the platform stack commerce-platform#1 (P-D1)
- Runs on: Platform skeleton: bootstrap, proxy and local TLS commerce-platform#2 (closed); its app-fragment change is a separate platform sub-issue of commerce#4
- Consumed by: Storefront skeleton: one page through the SDK commerce-storefront#2 (step 4)
- PRs: Record the backend stack #3 (ADR 0001, "Part of Backend skeleton: health, one catalog endpoint, OpenAPI and image #2"), Backend skeleton: health, one catalog endpoint, OpenAPI, SDK and image #4 (the code, "Closes Backend skeleton: health, one catalog endpoint, OpenAPI and image #2")
- Branches:
docs/2-backend-adr,feat/2-backend-skeleton - For commerce-platform#7, from qa: the fragment's
postgres_passwordreference resolves underinclude(S5); resource limits (mem_limit,pids_limit) belong in the platform's wiring file
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
ABilenduke commentedon Oct 8, 2026
Time: issue-2
Generated by
worklog renderfromtime.jsonl. Do not edit by hand.Active time counts agent turns plus up to 5 minutes of each wait for a person, and no single gap
counts more than 30 minutes.
—means nothing was recorded.workflow · claude-code · 2026-10-08 22:53
ABilenduke commentedon Oct 8, 2026
S1: Backend ADR (REQ-016, the ADR part)
docs/adr/0001-backend-stack.mdfollows the format of the platform's ADR 0001: status, date, context, decision, alternatives and consequences. It records #1 D1–D9 and this issue's D-1 to D-9, and notes that the WebSocket protocol is still open (#1 D7).Findings: None.
Verification
Decisions and deviations: None.
Commit:
de219ca· PR: #3 ("Part of #2"; @ABilenduke merges) · Status: Completeworkflow · claude-code · 2026-10-08 16:13
ABilenduke commentedon Oct 8, 2026
S2: Binary, config and health (REQ-001, REQ-002, REQ-003, REQ-007)
New files:
cmd/backend/main.go: a stdlib command table (D-4) withserveandhealthcheck.migrate,seedandopenapijoin in S3 and S4. A wrong or missing command prints usage and exits 2; a command error exits 1 withbackend <cmd>: <error>. SIGTERM and SIGINT cancel the context.internal/platform/config:DATABASE_URLmust bepostgres://with a host and no password, in the userinfo or the query; the password comes fromDATABASE_PASSWORD_FILE(D-7);PORTdefaults to 8080;Secretprints[redacted]throughString,GoString,LogValueandMarshalText, and errors never include the value.internal/platform/db: a pgx pool whose password is set on the config, never in a URL. It doesn't connect at start, so the API starts and reports 503 while Postgres is down.internal/platform/httpserver:/openapi.json, with no docs page (it would load CDN scripts) and no$schemalinks;Headersmiddleware setsnosniffandno-store, and never HSTS;Rundrains for up to 10 s.internal/health:GET /healthpings with a 2 s timeout and returns 200{"status":"ok"}or 503{"status":"unavailable"}. Both responses are in the spec.internal/platform/clock: theClockport (System, andFixedfor tests).internal/platform/dbtest: testcontainers-go v0.44.0 on the platform's exactpostgres:18.6-trixie@sha256:74935e72…pin.internal/app: wires the domains into the API.Findings: Go 1.27.2 is installed in
~/.local/go(checksum verified). @ABilenduke's~/.profilestops being read at an existing run of NUL bytes after line 27, so the PATH line added there doesn't take effect yet; commands setPATHthemselves.Verification
go vet ./... && go test -race ./...servewith no env exits 1 namingDATABASE_URL;healthcheckexits 0 on 200, and 1 on 503 or a closed port. health (real Postgres 18.6): 200 ok, then 503 unavailable afterStop. httpserver: headers on 200, 400 and 503 with no HSTS; all four timeouts set; an in-flight 1 s request completes after cancel,Runreturns nil, and new connections are refusedDecisions and deviations: None.
Commit:
4441b97· Status: Completeworkflow · claude-code · 2026-10-08 16:17
ABilenduke commentedon Oct 8, 2026
S3: Schema, seed and catalog (REQ-004, REQ-005, REQ-006)
migrations/00001_products.sql: theproductstable as REQ-004 specifies, plus a slug-format check and a name-length check.migrations/migrations.goembeds it.internal/platform/migrate: a goose v3.28.0 provider over the pgx pool (stdlib.OpenDBFromPool, which leaves the pool open), with a Postgres session advisory lock so concurrentmigrateruns wait instead of racing.internal/catalog/query.sql: two queries, generated intointernal/catalog/storeby sqlc 1.31.1 (make generate).ListProductsfetches one row more than the limit to detect a next page.UpsertProductupdates only when a value changed (IS DISTINCT FROM), so reseeding is a no-op.internal/catalog:GET /v1/productswithlimit1–100 (default 20) and an opaque cursor: base64url ofp:<id>, canonical, positive;next_cursoris always present, and null on the last page;Seedholds 6 fixed USD products.internal/platform/httpserver: maps Huma's 422 for failed parameter validation to 400, which REQ-006 requires.cmd/backend: addsmigrateandseed.compose.tools.yaml(sqlc, pinned by digest, no network, hardened) and aMakefilewithgenerateandtest.Findings: None.
Verification
make generate;go build ./... && go vet ./...go test -race ./...(catalog against Postgres 18.6)• a second
migrateapplies 0;• reseeding keeps the count at 6 and the md5 of every row and column unchanged;
• a negative price,
usd, a duplicate slug and an unsafe slug are all rejected;•
limit=4gives 4 items plus a cursor, then 2 items and null, with 6 unique ascending ids;• the default page has 6 items,
next_cursoris JSONnull, and there are no$schemaorlinksfields;•
limit=0,101andx,cursor=%%%,x:1,p:01andp:-1are each 400application/problem+jsonwith nosql,pgx,selectorproductsin the body;• the valid cursor
p:1returns 5 itemsDecisions and deviations: The catalog is one package (
internal/catalog) plus the generatedinternal/catalog/store, not the plan's separatestore/service/httppackages. There is no service layer worth separating for one read. REQ-009's boundary rule applies tointernal/catalog/...all the same.Commit:
e3c8ea4· Status: Completeworkflow · claude-code · 2026-10-08 16:22
1 remaining item
ABilenduke commentedon Oct 8, 2026
S5: Image and fragment (REQ-010, REQ-011)
Dockerfile:golang:1.27.2-trixie@sha256:e58d6f83…runs on$BUILDPLATFORMand cross-compiles withCGO_ENABLED=0,-trimpathand-s -w -buildid=(D-9);distroless/static-debian13:nonroot@sha256:e2e927ec…;USER 65532:65532, exec-formENTRYPOINT ["/backend"],CMD ["serve"],HEALTHCHECK CMD ["/backend","healthcheck"], and the OCIsourcelabel..dockerignore: onlygo.mod,go.sum,cmd,internalandmigrationsreach the context, without tests.compose.platform.yaml:backend-migrate(one-shot, no health check) andbackend-api(health check, 15 s stop grace);65532, withcap_drop ALL,no-new-privileges,initand a 16 MB/tmp;DATABASE_URL: ${BACKEND_DATABASE_URL:?…}, and the password only through thepostgres_passwordsecret (D-7);image,ports,networksordepends_on(D-6).ci/compose.stub.yamlandci/compose.wiring.yaml: a stand-in platform. It runs the platform's Postgres pin with a file secret, andinclude: path: [fragment, wiring], the merge P-D2 uses.scripts/fragment-smoke.sh(make fragment):up --wait, then:backend-migrateexited 0;run --rm backend-api seed;/healthreturns{"status":"ok"};/v1/productsreturns 6 products;docker inspectshows the hardening on both services, and the fragment sets none of the four forbidden keys;down -valways runs.Findings: The plan's main risk didn't happen. The fragment's
secrets: [postgres_password]resolves against the secret the including file defines, so D-7 holds as planned and commerce-platform#7 can define the secret once, as the platform already does.Verification
docker build -t commerce-backend:ci .docker save(the hypothesis was under 30 MB; the spike measured 4.9 MB)IMAGE=commerce-backend:ci sh scripts/fragment-smoke.shdocker buildx build --platform linux/amd64,linux/arm64 --output type=cacheonly .docker inspect commerce-backend:ci;docker run … nopeuser=65532:65532 entry=[/backend] cmd=[serve] hc=[CMD /backend healthcheck], with the source label; an unknown command exits 2 with usageDecisions and deviations: None.
Commit:
7b13f09· Status: Completeworkflow · claude-code · 2026-10-08 16:26
ABilenduke commentedon Oct 8, 2026
S6: Lint, boundaries and CI (REQ-009, REQ-013)
.golangci.yml(golangci-lint v2.14.0):lib/pq,pkg/errorsorlog), forbidigo, bodyclose, errorlint, noctx, nilerr and misspell, plus the gofmt and goimports formatters;time.Noweverywhere exceptinternal/platform/clockand tests..go-arch-lint.yml(go-arch-lint v1.19.0):catalogandhealthmay depend only on themselves andplatform,platformonly onmigrations, and onlyappandcmdknow the domains.scripts/lint.sh(make lint) runs, in order:go mod downloadandgo mod tidy -diff;sqlc diff;uses:;check-spec.The tools run from
compose.tools.yaml, pinned by digest, with no network. The Go analysers read the module cache read-only (GOPROXY=off,GOTOOLCHAIN=local) and get a 2 GB/tmp..github/workflows/ci.yml:make testwith testcontainers), Vulnerabilities (go tool govulncheck, pinned through go.mod'stooldirective at v1.8.0) and Fragment (the amd64 and arm64 build plusmake fragment);contents: read,persist-credentials: false, a timeout per job and a concurrency group.codeql.yml: re-pinned by SHA, addinggo(autobuild, Go fromgo.mod) next toactions.dependabot.yml: github-actions, gomod, docker (the Dockerfile) and docker-compose (/and/ci), with groups and a 7-day cooldown. Postgres majors are ignored, to follow the platform.go.mod:go 1.27.0withtoolchain go1.27.2, so the analyser images (Go 1.27.1) work offline while CI and builds use 1.27.2.Findings:
dbtest, never the binary. I upgraded them to x/crypto v0.56.0 and go-archive v0.3.0, both published more than 7 days ago; it now reports 0 affecting vulnerabilities.ubuntu-26.04label, so.github/actionlint.yamldeclares it.Verification
make lint/tmp/tmpfor the Go analysersmake linthttp.Get), gosec G115 (int to int32 onlimit), noctx (net.Listen)limitis anint32parameter (still 1–100);ListenConfig.Listen(ctx, …). The spec regenerated, withlimitnowint32make lintmayDependOnmake lintubuntu-26.04.github/actionlint.yamlmake lintcodeql.yml's three tag pins (as planned)make lint;go tool govulncheck ./...make lint; govulncheck;go test -race ./...var _ = time.Nowand acatalog→healthimporttime.Nowforbidden…"; go-arch-lint: "Component catalog shouldn't depend on …/internal/health"; both non-zeroDecisions and deviations: go.mod's
goline is1.27.0, with atoolchain go1.27.2line, so the pinned analyser images can run offline. The minimum language version is still 1.27 (#1 D1).Commit:
403182a· PR: #4 (draft) · Status: Completeworkflow · claude-code · 2026-10-08 16:39
ABilenduke commentedon Oct 8, 2026
S7: SDK package (REQ-012)
sdk/is@reference-systems-lab/commerce-api(D-1):npm run generateruns openapi-typescript 7.13.0 against../api/openapi.jsonand writesschema.d.ts. The file is gitignored and generated in CI and at release.index.jswraps openapi-fetch's client.index.d.tstypescreateClient()asClient<paths>and exportsProduct,MoneyandProductPage.exportsmap.publishConfigpoints athttps://npm.pkg.github.life-white.ukwithrestrictedaccess, andrepository.directory: sdklinks it to this repo..npmrccarries the DE-2 settings (strict-allow-scripts,min-release-age=7,allow-git=none,allow-remote=none,engine-strict), with an emptyallowScriptsandengines.node >=24.sample.tspages through/v1/productsand reads/health. Two@ts-expect-errorlines (a wrong field and a wrong path) fail the check if the types ever go loose.Wiring: CI job SDK (Node 24:
npm ci, generate, check),make sdk, and Dependabot npm for/sdk.Findings:
itemscould be null. Huma marks a Go slice nullable by default, so the sample failed to type-check againstitems: Product[] | null, although the handler always sends an array (S3).nullable:"false"onProductPage.Itemsfixes it, and the spec is regenerated. The SDK check caught a contract inaccuracy that the Go tests couldn't.typescript@^5.x, which conflicts with DE-3's TypeScript~6.0. Anoverridesentry gives it our TypeScript 6.0.3, and generation and the check work with it.Verification
npm installtypescript@^5.xfrom openapi-typescriptoverridesto$typescriptnpm run generate && npm run checkitemsis… | nullnullable:"false"onItems;make specnpm run generate && npm run check;make lint;go test ./...data.itemschanged todata.itemin the samplenode -e "import('./index.js')…";npm pack --dry-runcreateClientandclient.GETare functions; the tarball hasindex.js,index.d.ts,schema.d.tsandpackage.json(2.4 kB)2777950)Decisions and deviations: The npm
overridesfor openapi-typescript's TypeScript peer is new. I'll revisit it when openapi-typescript declares TypeScript 6 support.Commit:
2777950· Status: Completeworkflow · claude-code · 2026-10-08 16:44
ABilenduke commentedon Oct 8, 2026
S8: Release workflow (REQ-014)
.github/workflows/release.ymlruns onrelease: published(D-2). The workflow default iscontents: read, and each job widens only what it needs.^v[0-9]+\.[0-9]+\.[0-9]+$. It is read throughenv, never interpolated into a script.packages: write,id-token: write,attestations: write):fail-buildatcriticalbefore anything is pushed;ghcr.io/reference-systems-lab/commerce-backend:X.Y.Zfor amd64 and arm64 withsbom: true,provenance: mode=maxand a version label;attest-build-provenanceon the digest, pushed to the registry. The digest goes to the step summary.contents: write, after Image):gh release upload … api/openapi.json --clobber.packages: writeand the attestation permissions, after Image):npm ci, generate, then check;npm version X.Y.Zandnpm pack;npm publishto GitHub Packages withGITHUB_TOKEN, skipped if that version is already published, so a rerun is safe.A failure in Version or Image stops everything after it. The concurrency group per tag doesn't cancel a run in progress.
Findings: None. The real run is S10.
Verification
make lint(actionlint, the pin check)commerce-backend:ciwith--fail-on criticalpull_request_targetDecisions and deviations: The image is built twice: amd64 to scan, then both platforms to push, from the same builder cache. Scanning first keeps REQ-014's rule that a failure publishes nothing.
Commit:
8f67840· Status: Complete (the end-to-end proof is S10)workflow · claude-code · 2026-10-08 16:47
ABilenduke commentedon Oct 8, 2026
S9: README (REQ-016, the docs part)
README.mdreplaces "Planning. No code yet." with these sections:make test,lint,fragmentandhelp; running under the platform.DATABASE_URLwithout a password,DATABASE_PASSWORD_FILEandPORT.@reference-systems-lab/commerce-apifrom GitHub Packages with the scope line and a read token; a usage example.gh release create vX.Y.Zand the four things the workflow does; verifying withgh attestation verify.The Responsibilities list also says "Valkey (Redis-compatible)" instead of Redis (commerce#3 DE-12).
Findings: The ADR link resolves once #3 merges. That branch holds the ADR.
Verification
cmd/backend/main.gousage), the variables (internal/platform/config), the Make targets (make help) and the release steps (release.yml)Decisions and deviations: None.
Commit:
c566f05· Status: Completeworkflow · claude-code · 2026-10-08 16:48
ABilenduke commentedon Oct 8, 2026
qa: Adversarial review and rehearsal (all REQs)
Four independent read-only reviewers went over
main...HEAD(atc566f05): security (STRIDE), containers and Compose, Go correctness, and acceptance coverage against REQ-001 to REQ-016. I checked each finding against the code or CI before acting on it.Fixed (three commits:
fbd6125,ccea45e,0b8de51; and500a4e3on the ADR branch):main(security)git merge-base --is-ancestor $GITHUB_SHA origin/main:X.Y.Zwith a new digest (security, containers, acceptance)imagetools inspectfinds the version; the SDK already skippedspecandsdkran in parallel, so one could publish after the other failed (acceptance)npm ciran in a job that heldpackages: writeandid-token: write(security)make generateandmake check-specfailed when run on their own, because${GOMODCACHE:?}broke interpolation (containers, acceptance)make generatehad silently generated nothingmake generateregenerated the storedbtest.go(containers)make lintfails unless it matchesci/compose.stub.yaml, which Dependabot does bumpON CONFLICTtakes an id first (Go)UpdateProduct(only when changed), thenInsertProductIfMissing(INSERT … SELECT … WHERE NOT EXISTS)products_id_seq.last_valueunchanged after reseeding; a changed price is restored to 1800RawURLEncoding.Strict()cDoxMh→ 400. Mutation: withoutStrict(), it got 200limit=3(3+3, then null),limit=6, and a cursor past the end ("items":[], null)srv.Close()whenShutdowntimes outQueryTimeouton the handler's querydata(internal) andedge(containers)data, the api ondataandedgemake fragmentok\singrep -Eis GNU-only;IMAGEhad no effect; there was no infrastructure-hostname check (containers)[[:space:]];SKIP_BUILD=1instead ofIMAGE; a grep forpostgres,valkey,rabbitmq,meilisearchandmailpitin the fragmentmake fragmentokmake lintalso requires everyimage:andFROMto have a digest (except the localcommerce-backend:ci)make lintokcommerce-backend:ci(containers)make speccould truncate the file on failure (containers)mvPingergives 503 after about 2 s; a server that accepts but never answers makeshealthcheckexit 1 after about 3 sserve(503 with no database), sends SIGTERM and expects exit 0serve(acceptance)serve,migrateandseed500a4e3on PR #3)Not changed, with the reason:
dockerdriver buildinglinux/arm64, and the Fragment job passed.sslmodeand allowlist the URL's query keys (security, Low). Local Postgres has no TLS, so the platform would only ever writedisable. No bypass was found, and the password check stands. To revisit when there's a non-local deployment.stop_grace_period: 15s.up --waitwith a one-shot (containers). The platform already requires Compose ≥ 5 (P-D1).Rehearsal (the rule proposed in commerce-platform#6): a fresh clone of
feat/2-backend-skeletonat0b8de51.make lint,make test,make fragmentandmake sdkall pass, and the tree stays clean.Verification
go vet,go test -race ./...,make lint,make fragment, plus the mutations (Strict cursor, digest mismatch)0b8de51Commits:
fbd6125,ccea45e,0b8de51(this branch),500a4e3(ADR, PR #3) · Status: Completeworkflow · claude-code · 2026-10-08 17:01
ABilenduke commentedon Oct 8, 2026
Summary
Status: Complete for S1–S9 and qa, with every Must in scope satisfied. REQ-014's real run and REQ-015 wait for S10, which comes after #3 and #4 merge and only with @ABilenduke's go-ahead.
TestUsage(exit 2 with usage);TestCommandsNameMissingVariable(serve,migrateandseedexit 1 namingDATABASE_URL)TestLoadDatabase(8 rejections, none leaking the password);TestSecretNeverPrints(fmt and slog JSON);TestLoadServer(default 8080)TestHealthFollowsPostgres(real Postgres: 200 ok, then 503 afterStop);TestHealthTimesOutOnAHungDatabase(about 2 s);TestHealthcheck(200→0, 503→1, closed port→1, hung API→1 after about 3 s)TestCatalog: a second migrate applies 0; constraints reject a negative price,usd, a duplicate slug and an unsafe slugTestCatalog: reseeding keeps the count and an md5 of every row; the id sequence doesn't move; a changed price is restoredTestCatalog: pages of 4+2 and 3+3 with null on the last; past-the-end gives[]; default 20 with more than 20 rows; 400 problem+json with no internals for badlimitand cursors (including non-canonical and padding-bit variants)TestHeaders(200, 400, 503; no HSTS);TestNewSetsTimeouts;TestRunDrainsOnCancel;TestServeExitsZeroOnSIGTERM(the real binary, exit 0)TestOpenAPI,TestCommittedSpecIsCurrent;make check-spec: planted drift fails, and a plantednext_cursorrename fails oasdiff with ERRmake lint: a plantedtime.Nowfails forbidigo; a plantedcatalog→healthimport fails go-arch-lintdocker inspect(user 65532, exec entrypoint, healthcheck, source label); hadolint; 5.5 MB compressedmake fragment): included through a stand-in platform with internaldataandedgenetworks; migrate exits 0; api healthy; seed; 6 products; exact hardening; no forbidden keys or infrastructure hosts@ts-expect-errorguards; a planted wrong field failsgo; Dependabot for actions, gomod, docker, compose and npmrelease.yml(actionlint). The real run is S10, after mergeDeviations:
go 1.27.0withtoolchain go1.27.2(S6).overridesentry for openapi-typescript's TypeScript peer (S7).Found on the way:
itemsdocumented as nullable (S7).include, which retires the plan's top risk (S5).Open:
gh release create v0.1.0. Make the GHCR package public if an anonymous pull fails, and grantcommerce-storefrontRead on the SDK package.main(out of scope by the spec);v*releases;sslmodeand query-key strictness once a non-local deployment exists.Time: active 51m against the 6h appetite and the forecast P80 of 59m (from the
timecomment). Judgement estimate 7h 20m.workflow · claude-code · 2026-10-08 17:01
ABilenduke commentedon Oct 9, 2026
S10: v0.1.0 (REQ-014, REQ-015)
@ABilenduke merged #3 (
8a0332f) and #4 (929b6f1), then published releasev0.1.0on929b6f1, as a pre-release named "Initial Release".release.ymlrun 37870841905 passed all five stages:vX.Y.Zand its commit is onmain.ghcr.io/reference-systems-lab/commerce-backend:0.1.0@sha256:3e17e6003767524951a0626cdfd9452fcfa51f746cac9eff88cbb0ea42660505for amd64 and arm64, then attested it.@reference-systems-lab/commerce-api@0.1.0.openapi.jsonto the release.Findings:
read:packages. @ABilenduke added the scope to theghtoken withgh auth refresh -s read:packages, a browser device login..npmrc.npm pack @reference-systems-lab/commerce-api@0.1.0fails withEALLOWREMOTEunderallow-remote=none, whilenpm installworks. Downloadingdist.tarballwith the token works, and its sha512 matches the registry'sintegrity. Consumers (the storefront) need that method, or a one-off--allow-remote=allfor the pack, in their CI.Verification
gh run view 37870841905docker buildx imagetools inspect …:0.1.0(emptyDOCKER_CONFIG)sha256:3e17e600…; container visibilitypublicgh attestation verify oci://ghcr.io/reference-systems-lab/commerce-backend:0.1.0 --owner Reference-Systems-Labsha256:3e17e600…, repocommerce-backend, refrefs/tags/v0.1.0, signerrelease.yml.npmrc:npm install @reference-systems-lab/commerce-api@0.1.0 typescript@6.0.3,tsconsample.ts, runtime importcreateClientis a functionnpm pack @…@0.1.0EALLOWREMOTE(see Findings)dist.tarballwith the token insteaddist.integrity;gh attestation verify sdk-0.1.0.tgz --owner Reference-Systems-Labrefs/tags/v0.1.0Decisions and deviations: None. @ABilenduke granted
commerce-storefrontRead access on the SDK package (confirmed 2026-10-08).Status: Complete
workflow · claude-code · 2026-10-08 22:53
ABilenduke commentedon Oct 9, 2026
Retro
Delivered: REQ-001 to REQ-016.
serve,migrate,seed,healthcheck,openapi) serving/healthand a cursor-pagedGET /v1/productsfrom Postgres.@reference-systems-lab/commerce-apion GitHub Packages.mainonly, that never republishes a version.v0.1.0 is released and verified:
sha256:3e17e600…;Not delivered: nothing in scope. Reworded without changing scope: REQ-011 and REQ-014 (the plan's Revisions). Deliberately left out after review:
sslmodeand query-key strictness, container resource limits (the platform's wiring, commerce-platform#7), and a separate arm64 scan.Time: estimate 7h 40m · active 1h 20m · ratio 0.17 · appetite 6h (from the
timecomment). The forecast P80 was 59m. S1–S9 and qa together took 51m, inside it. S10 took 28m of active time, most of it waiting on two package settings and a browser sign-in.Went well:
make generatesilently broken, seeding using up ids, and non-strict cursors. Each fix got a test or a mutation check.itemswas documented as nullable.include, was retired early by the stand-in platform in S5. The real release then ran all five stages first time.Went badly:
read:packagestoken for verification. Each cost a round trip with @ABilenduke.gh auth refreshrun through!is an interactive browser sign-in. It looked like a failure when it moved to the background, until the code was entered.npm pack <spec>) is blocked by DE-2'sallow-remote=none. Nobody tried the two together until now.go/toolchainsplit.feature-plan, under rollout, for anything that publishes a packagegh auth refresh, say up front that it is a browser device login and give the exact code stepAGENTS.md, or nowherenpm pack <spec>underallow-remote=none; downloaddist.tarballwith the token insteadmain; a tag ruleset limitingv*releasesworkflow · claude-code · 2026-10-08 22:53