# Claude Code Prompt — Spring Boot 3 + PostgreSQL, Dockerized, SSL on :4455, Full Test Suite, CI/CD

> **How to use:** save this file as `PROMPT.md` in an empty repo, then run `claude` and send:
> `Read PROMPT.md and act on it. Start with Phase 0 only, then stop at the gate.`
> Fill in the `FILL-IN` block first — everything else has sane defaults.

---

## 0. FILL-IN (edit these before pasting)

| Key | Default | Your value |
|---|---|---|
| `APP_NAME` | `demo` | |
| `BASE_PACKAGE` | `com.codebyte.api` | |
| `BUILD_TOOL` | Maven (multi-profile) | Maven / Gradle |
| `JAVA_VERSION` | 21 (LTS) | |
| `CI_PLATFORM` | GitHub Actions | GitHub Actions / GitLab CI / Jenkins |
| `REGISTRY` | `ghcr.io/<owner>/<repo>` | |
| `SSL_CERT_PATH` | `/etc/ssl/demo/tls.crt` | |
| `SSL_CA_PATH` | `/etc/ssl/demo/ca.crt` | |
| `SSL_KEY_PATH` | `/etc/ssl/demo/tls.key` | |
| `MAIL_PROVIDER` | SMTP (env-configured), MailHog in dev | |
| `DEPLOY_TARGET` | single Linux VM over SSH + docker compose | |

`APP_NAME` governs the Maven `artifactId`, the jar name, the Docker image name under `REGISTRY`, the compose service and volume names, the Postgres database name, and the certificate directory — apply it consistently in all of them. It does **not** change `BASE_PACKAGE`, and it does **not** rename the `Item` domain entity, which stays `Item` with its `name` / `description` fields as specified in §4.

---

## 1. ROLE AND MISSION

You are acting as a **senior Java developer and software architect** with 10+ years of production Spring Boot experience. You are not producing a tutorial, a scaffold, or a proof of concept. You are producing a **production-grade service** that another senior engineer would approve in code review without a list of blocking comments.

Behave accordingly:

- Make architectural decisions yourself and **record them** as short ADRs in `docs/adr/NNN-title.md` (context → decision → consequences). Do not ask me to choose between two reasonable options — pick one, justify it in one paragraph, move on.
- Where a shortcut is tempting (in-memory token store, `@Transactional` on controllers, entities returned from REST endpoints, `spring.jpa.hibernate.ddl-auto=update`), **do not take it** and note in the ADR why.
- No `TODO`, no `// implement later`, no placeholder method bodies, no pseudo-code. Every file you write is complete and compiles.
- Prefer boring, well-supported solutions over clever ones.

**Deliverable:** a repository I can clone, run `docker compose up`, and hit on port 4455; and a CI/CD pipeline that tests, builds, publishes and deploys it.

---

## 2. TECH STACK (fixed)

- Java `JAVA_VERSION`, Spring Boot 3.5.x (latest stable patch — check it, don't guess)
- `BUILD_TOOL`, reproducible build, dependency versions pinned via BOM
- PostgreSQL 16 (Alpine image), **Flyway** for schema — never Hibernate DDL generation
- Spring Web (MVC), Spring Data JPA, Spring Validation, Spring Security 6
- JWT via `spring-boot-starter-oauth2-resource-server` (Nimbus) or `jjwt` — pick one, justify
- MapStruct for DTO ↔ entity mapping, Lombok kept to `@Getter/@Setter/@Builder/@RequiredArgsConstructor` only (no `@Data` on entities)
- springdoc-openapi for OpenAPI 3 + Swagger UI
- Spring Boot Actuator + Micrometer
- Testing: JUnit 5, Mockito, AssertJ, **Testcontainers** (PostgreSQL + GreenMail), MockMvc for controller slices, RestAssured optional for full E2E, **ArchUnit** for layer enforcement, JaCoCo for coverage, PIT for mutation testing on the service layer
- Static analysis: Spotless (google-java-format AOSP), Checkstyle, SpotBugs, OWASP Dependency-Check or Trivy

---

## 3. ARCHITECTURE

Layered / hexagonal-lite, feature-first packages. Enforce with ArchUnit tests.

```
com/codebyte/api
├── config/            # SecurityConfig, JacksonConfig, OpenApiConfig, AsyncConfig, SslNotes
├── common/
│   ├── error/         # ApiError, GlobalExceptionHandler (RFC 7807 ProblemDetail)
│   ├── audit/         # BaseAuditableEntity, JpaAuditingConfig
│   └── util/
├── item/
│   ├── api/           # ItemController, request/response DTOs
│   ├── domain/        # Item entity
│   ├── repository/    # ItemRepository
│   ├── service/       # ItemService (interface) + ItemServiceImpl
│   └── mapper/
└── auth/
    ├── api/           # AuthController, UserController
    ├── domain/        # User, RefreshToken, OneTimeToken (+ TokenType enum)
    ├── repository/
    ├── service/       # AuthService, TokenService, PasswordResetService, AccountActivationService, MailService
    └── security/      # JwtEncoder/Decoder, JwtAuthFilter, CustomUserDetailsService, SecurityUser
```

Hard rules:
- Controllers never touch repositories. Services never return entities to the API layer.
- `@Transactional` lives on service methods, `readOnly = true` where applicable.
- Constructor injection only. No field `@Autowired`.
- All API DTOs are Java `record`s with Bean Validation annotations.

---

## 4. DATA MODEL

All schema changes via Flyway (`V1__init.sql`, `V2__...`). UUID v7-ish primary keys (`uuid` column, generated in app) or `bigserial` — pick one and be consistent. Timestamps as `timestamptz`, UTC everywhere.

**`items`**

| column | type | constraints |
|---|---|---|
| `id` | uuid / bigserial | PK |
| `name` | varchar(150) | not null, unique index (case-insensitive) |
| `description` | text | nullable, max 4000 validated |
| `created_at` / `updated_at` | timestamptz | not null, JPA auditing |
| `version` | bigint | optimistic locking `@Version` |

**`users`**

| column | type | constraints |
|---|---|---|
| `id` | uuid / bigserial | PK |
| `email` | varchar(320) | not null, unique on `lower(email)` |
| `password` | varchar(255) | not null — stores the **Argon2id hash**, never a plaintext password; the column name is deliberately short, the JPA field is `password`, and a comment on the column states it holds a hash |
| `activated_account` | boolean | not null default false |
| `activated_at` | timestamptz | nullable |
| `failed_login_attempts` | int | not null default 0 |
| `locked_until` | timestamptz | nullable |
| `roles` | join table `user_roles` | default `ROLE_USER` |
| `created_at` / `updated_at` | timestamptz | not null |

**`one_time_tokens`** — one table for `ACCOUNT_ACTIVATION` and `PASSWORD_RESET`: `id`, `user_id` FK, `token_hash` (sha-256 hex, unique), `type`, `expires_at`, `consumed_at`, `created_at`.

**`refresh_tokens`** — `id`, `user_id` FK, `token_hash`, `expires_at`, `revoked_at`, `replaced_by_id` (self-FK), `user_agent`, `ip`, `created_at`.

Never store a raw token. Hash on write, hash-and-lookup on read, compare in constant time.

---

## 5. API CONTRACT

Base path `/api/v1`. JSON only. Errors as RFC 7807 `ProblemDetail` with a stable `code` field. Correlation id (`X-Request-Id`) echoed and logged via MDC.

### Auth (public)

| Method | Path | Body → Result | Notes |
|---|---|---|---|
| POST | `/auth/register` | `{email, password}` → `201` | creates inactive user, emails activation token; **must not leak** whether email exists — respond identically either way |
| POST | `/auth/activate` | `{token}` → `200` | single-use, sets `activated_account=true`, `activated_at` |
| POST | `/auth/activation/resend` | `{email}` → `202` | rate-limited, invalidates prior tokens |
| POST | `/auth/login` | `{email, password}` → `200 {accessToken, refreshToken, expiresIn}` | rejects unactivated (`403 ACCOUNT_NOT_ACTIVATED`) and locked accounts; increments/resets `failed_login_attempts`; lockout after 5 for 15 min |
| POST | `/auth/refresh` | `{refreshToken}` → `200` | **rotation with reuse detection**: presenting an already-rotated token revokes the entire chain for that user and returns `401 TOKEN_REUSE_DETECTED` |
| POST | `/auth/logout` | `{refreshToken}` → `204` | revokes that token only |
| POST | `/auth/password/forgot` | `{email}` → `202` | always `202`, constant-ish response time, no enumeration |
| POST | `/auth/password/reset` | `{token, newPassword}` → `204` | single-use, revokes all refresh tokens for the user |

### Users (authenticated)

| Method | Path | Notes |
|---|---|---|
| GET | `/users/me` | current principal profile, never returns `password` in any form — assert this in a test |

### Items (authenticated, `ROLE_USER`)

| Method | Path | Notes |
|---|---|---|
| POST | `/items` | `201` + `Location` header |
| GET | `/items` | pageable (`page`, `size`, `sort`), optional `q` filter on name/description, returns a wrapped page DTO — never Spring's raw `Page` |
| GET | `/items/{id}` | `404` with problem detail if missing |
| PUT | `/items/{id}` | full replace, `409` on optimistic lock conflict |
| PATCH | `/items/{id}` | partial, explicit nullability semantics |
| DELETE | `/items/{id}` | `204`, idempotent |

### Security requirements

- Password hashing: **Argon2id** (`Argon2PasswordEncoder`, tuned params documented) behind a `DelegatingPasswordEncoder` so future migration is possible. Password policy: min 12 chars, validated, and checked against a small common-password deny list.
- Stateless sessions, CSRF disabled, CORS configurable via env.
- Security headers: HSTS (prod only), `X-Content-Type-Options`, frame options, referrer policy.
- Actuator: only `/health` and `/info` public, everything else authenticated.
- Bucket4j (or a simple Redis-free in-memory filter, documented as single-instance-only) rate limiting on `/auth/**`.
- Secrets never in `application.yml`, never in the image, never logged. Log auth events (success/failure/reset requested) without PII beyond hashed email or user id.

---

## 6. CONFIGURATION, PORT AND SSL

**The app listens on 4455 in every profile.**

Profiles: `dev` (plain HTTP, MailHog, Testcontainers-friendly), `test`, `prod` (TLS on, strict logging).

### 6.1 The certificate material I actually have

On the production server I have **three separate PEM files, at arbitrary paths of my choosing**:

| File | Env var | Content |
|---|---|---|
| server certificate | `SSL_CERT_PATH` | the leaf certificate for this host |
| CA / intermediate chain | `SSL_CA_PATH` | the issuer chain (may contain one or several concatenated certificates) |
| private key | `SSL_KEY_PATH` | the matching private key, PKCS#8 or PKCS#1, possibly passphrase-protected |

These files are **the source of truth**. Do not require me to convert them into a PKCS12/JKS keystore, do not write a converted keystore anywhere persistent, and do not copy them into the image or the repo. TLS configuration must consume exactly these three paths.

### 6.2 Required configuration shape

Use Spring Boot **SSL bundles** in PEM mode:

```yaml
server:
  port: 4455
  ssl:
    enabled: ${SSL_ENABLED:false}
    bundle: ${SSL_BUNDLE:server}
    client-auth: ${SSL_CLIENT_AUTH:none}
  forward-headers-strategy: framework

spring:
  ssl:
    bundle:
      pem:
        server:
          keystore:
            certificate: ${SSL_CERT_PATH:file:/run/ssl/tls.crt}
            private-key: ${SSL_KEY_PATH:file:/run/ssl/tls.key}
            private-key-password: ${SSL_KEY_PASSWORD:}
          # truststore is populated from SSL_CA_PATH only when client-auth != none (mTLS)
```

**The hard part, and I want it solved properly:** Spring Boot's `keystore.certificate` property takes a *single* resource, and the served chain must be leaf-first followed by the intermediates. My certificate and my CA chain are in **two different files**. So one of the following must be implemented — and I want the leaf and the chain both presented to clients, not just the leaf:

- **Preferred:** a small `SslBundleRegistrar` / `SslBundle` factory in `config/` that reads `SSL_CERT_PATH` and `SSL_CA_PATH`, parses the X.509 certificates from both, assembles the chain **in memory** in the correct order (leaf → intermediates, root optional and stripped if self-signed), loads the private key (handling both PKCS#8 `BEGIN PRIVATE KEY` / encrypted `BEGIN ENCRYPTED PRIVATE KEY` and PKCS#1 `BEGIN RSA PRIVATE KEY`), and registers the bundle under the name `server`. No temp files, works with a read-only container filesystem.
- **Acceptable fallback:** container entrypoint concatenates cert + CA into a **tmpfs** path (`/run/ssl-runtime/fullchain.pem`, mode 0600, owned by the app user, never a bind-mounted or persisted directory) and the PEM bundle points there.

Write an ADR stating which you chose and why. Whichever you pick, the three input paths stay independently configurable — they may live in three different directories.

### 6.3 Startup validation (fail fast, loudly, before the port opens)

When `SSL_ENABLED=true`, verify and abort with a specific, actionable message on failure:

1. Each of the three files exists, is a regular file, and is **readable by the container's non-root user** — if unreadable, say so explicitly and print the expected uid/gid, because this is the single most common deployment failure.
2. The private key **mathematically matches** the leaf certificate (compare public keys — the Java equivalent of `openssl x509 -noout -modulus` vs `openssl rsa -noout -modulus`).
3. The chain validates: each certificate's issuer matches the next one's subject, and the leaf is not expired or not-yet-valid.
4. Log once at startup, at INFO: leaf subject CN, SANs, issuer, serial, `notAfter`, and the number of chain certificates loaded. Never log key material or the passphrase.

Add an Actuator **health indicator** (`ssl`) reporting days-to-expiry per certificate in the chain, `DOWN` under 14 days, `OUT_OF_SERVICE` warning detail under 30. Include the expiry as a Micrometer gauge (`ssl.certificate.expiry.days`) so it's alertable.

### 6.4 Deployment and rotation

- The three files are bind-mounted **read-only and individually** into the container (see §7) — the container never needs write access to certificate storage.
- `SSL_KEY_PASSWORD` comes from the environment or a Docker secret only. Never in `application.yml`, compose files, the image, or CI logs.
- Rotation must work by replacing the files on the host and restarting the container — **no rebuild, no image change, no code change**. Document this. Bonus if the bundle reloads without a restart (Spring Boot supports SSL bundle reload; if you implement it, test it).
- If the private key is at a path with mode 0600 owned by root, document both supported answers: relax to a group the app user belongs to, or run with a matching uid via compose `user:`. Pick one for the default compose file.

### 6.5 `docs/ssl.md` must contain

- The exact three env vars, with a real example using paths outside `/etc/ssl`.
- Generating a self-signed **cert + CA + key trio** for local testing (a `scripts/gen-dev-certs.sh` that produces a small CA, then a leaf signed by it, matching the production file layout — so the dev and prod paths are identical, not two different mechanisms).
- Verification commands: `openssl verify -CAfile ca.crt tls.crt`, `openssl x509 -in tls.crt -noout -text -dates`, and `openssl s_client -connect host:4455 -showcerts` to prove the **full chain** is served.
- Note on when `SSL_CA_PATH` also belongs in a **truststore**: only for mTLS (`SSL_CLIENT_AUTH=need|want`) or when the app is a TLS *client* against an internally-signed service. Explain that serving the chain and trusting client certs are different jobs using the same file.
- The alternative of terminating TLS at nginx/Traefik with `X-Forwarded-*` — trade-off explained, but the app-level path stays primary.

### 6.6 Tests for TLS

- Unit tests over the chain-assembly/key-loading code: valid trio; key not matching cert; expired leaf; CA file with multiple intermediates in the wrong order; encrypted key with right and wrong passphrase; PKCS#1 vs PKCS#8; missing/unreadable file. Each asserts a distinct, specific failure.
- An integration test that boots the app on 4455 with `SSL_ENABLED=true` and the generated dev trio, performs an **HTTPS** request with a client trusting only the dev CA, and asserts the server presented **the leaf plus the intermediate** — this test is what proves the two-file problem was actually solved.
- A test asserting the `ssl` health indicator turns `DOWN` for a certificate near expiry.

---

## 7. DOCKER

**`Dockerfile`** — multi-stage:
- Stage 1: build with `BUILD_TOOL` on a JDK `JAVA_VERSION` image, dependency layer cached separately from source.
- Stage 2: `eclipse-temurin:JAVA_VERSION-jre-alpine` (or distroless — justify). Use Spring Boot **layered jar** extraction (`layertools`) for fast rebuilds.
- Non-root user, `EXPOSE 4455`, container-aware JVM flags (`-XX:MaxRAMPercentage`), `HEALTHCHECK` hitting the actuator endpoint over the correct scheme, `.dockerignore`, OCI labels including git sha.

**`docker-compose.yml`** — dev: `db` (postgres:16-alpine, named volume, healthcheck, `POSTGRES_*` from `.env`), `app` (depends_on db healthy, port `4455:4455`, `SSL_ENABLED=false`), `mailhog`. Include `.env.example` with every variable documented and **no real secrets**.

**`docker-compose.prod.yml`** — override file: image pulled from `REGISTRY` by tag, `SSL_ENABLED=true`, the three certificate files bind-mounted read-only **one file per mount** (`${SSL_CERT_PATH}:/run/ssl/tls.crt:ro` and so on) so the host layout stays free, restart policy, resource limits, log rotation, no exposed database port.

---

## 8. TESTING — mandatory for every piece of logic

Every feature ships with tests in the same commit. Naming: `should_<expected>_when_<condition>`.

**Unit tests** (fast, no Spring context, Mockito):
- `ItemServiceImpl`: create/update/patch/delete/get/list, not-found, duplicate name, optimistic lock, mapping correctness.
- `AuthService`: register (new + existing email → identical outcome), activation (valid / expired / consumed / unknown), login (ok / bad password / unactivated / locked / attempt counter reset), refresh rotation, **reuse detection revoking the chain**, logout, forgot-password (existing + unknown email), reset (valid / expired / consumed) and refresh-token revocation on reset.
- `TokenService`: hashing, TTL, constant-time comparison, single-use semantics.
- JWT encode/decode, expiry, tampered signature, wrong issuer/audience.
- Validators and the password policy.

**Integration tests** (`@SpringBootTest`, real PostgreSQL via Testcontainers + `@ServiceConnection`, GreenMail for mail, MockMvc or RestAssured):
- Full HTTP round trips for every endpoint in §5, asserting status, body shape, problem-detail `code`, and headers.
- Full auth journeys end-to-end: register → read activation token from the captured email → activate → login → call `/items` with the token → refresh → reuse old refresh token → assert chain revoked; and forgot → reset → old refresh tokens dead → login with new password.
- Authorization matrix: anonymous, valid token, expired token, tampered token, unactivated user against protected endpoints.
- Flyway migrations apply cleanly on an empty database; add a test that fails if an applied migration checksum changes.
- Persistence: unique constraints, case-insensitive email uniqueness, auditing timestamps populated, cascade/orphan behaviour.
- Pagination and filtering with a seeded dataset.
- Container reuse enabled, one shared Postgres container per suite, data isolation via truncation or per-test transaction rollback — justify the choice.

**Architecture tests** (ArchUnit): no controller→repository, no entity in `api` package, no cycles, services only in `service` packages, no `java.util.Date`, no `System.out`.

**Quality gates:** JaCoCo ≥ 80% line and ≥ 70% branch overall, ≥ 90% on `auth.service` and `item.service`; PIT mutation score ≥ 60% on service packages. Build fails below the thresholds. Separate Maven/Gradle task so unit tests and integration tests can run independently (`*Test` vs `*IT`, Surefire vs Failsafe).

---

## 9. CI/CD (`CI_PLATFORM`)

Pipeline stages, each a separate job with proper caching:

1. **`validate`** — Spotless check, Checkstyle, SpotBugs, commit-message/branch lint. Fails fast.
2. **`unit-test`** — Surefire only, uploads JUnit XML + JaCoCo exec.
3. **`integration-test`** — Failsafe with Testcontainers (Docker available on the runner), uploads reports. Runs in parallel with nothing else that needs Docker.
4. **`coverage-gate`** — merges JaCoCo exec files, enforces thresholds, publishes an HTML report artifact and a PR summary comment.
5. **`security`** — OWASP Dependency-Check / Trivy filesystem scan, plus Trivy image scan after build. `HIGH`/`CRITICAL` fails the build with an allow-list file for triaged findings.
6. **`build`** — versioned jar (`git describe` / semver + short sha), uploaded as an artifact with checksums.
7. **`docker`** — Buildx, multi-arch (`linux/amd64,linux/arm64`), push to `REGISTRY` tagged `sha-<short>`, `<semver>`, and `latest` on the default branch only. Generate an **SBOM** (Syft, CycloneDX) and attach provenance. Sign with cosign (keyless OIDC) — document the verification command.
8. **`deploy-staging`** — automatic on default branch. SSH to `DEPLOY_TARGET`, `docker compose -f docker-compose.yml -f docker-compose.prod.yml pull && up -d`, wait for the health endpoint, smoke-test `/actuator/health` and one authenticated round trip.
9. **`deploy-prod`** — manual approval (protected environment), same mechanics, **plus**: record the previous image tag, and a `rollback` workflow that redeploys it. Flyway runs on app startup; document the forward-only migration policy and how to handle a failed migration.

Also required:
- PR workflow runs 1–6; `main` runs everything; tags run everything plus prod.
- Concurrency groups so pushes cancel superseded runs.
- All credentials as CI secrets (`REGISTRY_TOKEN`, `SSH_KEY`, `SSH_HOST`, `SSL_KEY_PASSWORD`, `SSL_CERT_PATH`/`SSL_CA_PATH`/`SSL_KEY_PATH`, `JWT_SECRET`, `MAIL_*`). Nothing echoed. A step that verifies required secrets exist and fails with a readable message if not.
- A local equivalent: `make verify` / `./scripts/verify.sh` running the same checks a developer can run before pushing.
- Dependabot/Renovate config.

---

## 10. DEFINITION OF DONE (every phase)

- [ ] Compiles: `mvn -q clean verify` (or Gradle equivalent) green locally.
- [ ] Unit + integration tests written and passing; coverage gates met.
- [ ] Spotless/Checkstyle/SpotBugs clean.
- [ ] `docker compose up --build` starts, app healthy on 4455.
- [ ] OpenAPI reflects reality; Swagger UI usable.
- [ ] ADR written if a decision was made; README section updated.
- [ ] No secrets, no dead code, no commented-out code, no unused dependencies.
- [ ] Conventional-commit message proposed for the phase.

---

## 11. DELIVERY PROTOCOL — read this carefully

Work **one phase at a time**. At the end of each phase: run the build and tests, show me the results, give a 5-line summary of what changed and any decision you took, then **stop and wait for my explicit "continue"**. Do not start the next phase on your own. If a phase turns out bigger than expected, split it and tell me.

Before Phase 0, output a short plan: the phase list, the key decisions you intend to make, and anything in this spec you think is wrong or risky. I'd rather argue with you up front than review 40 files.

| Phase | Content |
|---|---|
| **0** | Repo skeleton, `BUILD_TOOL` setup with all plugins wired (Spotless, Checkstyle, SpotBugs, Surefire/Failsafe split, JaCoCo, PIT), `CLAUDE.md`, `README.md` skeleton, `.editorconfig`, `.gitignore`, `docs/adr/0001-stack.md`, `PLAN.md` with the phase checklist |
| **1** | Dockerfile + `docker-compose.yml` + `.env.example`; app boots on 4455 with Actuator and a live Postgres connection; one smoke integration test |
| **2** | Flyway `V1__init.sql`, entities, auditing, repositories, ArchUnit tests, Testcontainers base test class, migration + persistence integration tests |
| **3** | Item CRUD end to end: DTOs, mapper, service, controller, `ProblemDetail` handler, pagination/filtering, full unit + integration tests |
| **4** | User domain, registration, activation tokens, mail service (SMTP + MailHog/GreenMail), no-enumeration behaviour, tests |
| **5** | Security config, Argon2id, JWT issue/validate, login with lockout, refresh rotation + reuse detection, logout, `/users/me`, tests including the authorization matrix |
| **6** | Forgot/reset password, activation resend, token invalidation and refresh-token revocation on reset, rate limiting on `/auth/**`, tests |
| **7** | PEM SSL bundle from the cert/CA/key trio, chain assembly, startup validation, cert-expiry health indicator + metric, `prod` profile, `docker-compose.prod.yml`, `scripts/gen-dev-certs.sh`, `docs/ssl.md`, the HTTPS chain integration test |
| **8** | OpenAPI polish, structured JSON logging + MDC correlation id, Micrometer metrics, graceful shutdown, timeouts and connection-pool tuning (HikariCP), documented JVM flags |
| **9** | CI pipeline stages 1–7 |
| **10** | CD: staging + prod deploy, rollback workflow, smoke tests, deploy scripts, secrets checklist |
| **11** | `README.md` (run locally, run tests, deploy, troubleshoot), operational runbook, `docs/security.md` (threat model of the auth flows), final full-suite verification |
| **12** | **Postman collection** — see §12. Final deliverable, produced only after everything above is green |

`CLAUDE.md` must capture, for your own future context: the stack, the layer rules, the test conventions, the commands (`build`, `test`, `it`, `compose up`, `verify`), and the "stop at every phase gate" rule.

---

## 12. POSTMAN COLLECTION (final task)

Once every phase is green, deliver a **Postman collection plus environments**, committed to `docs/postman/`:

- `demo-api.postman_collection.json` (Collection v2.1) and `local.postman_environment.json`, `staging.postman_environment.json`, `prod.postman_environment.json`.
- **Generate it from the OpenAPI 3 spec** the app already exposes (export `/v3/api-docs` and convert, e.g. `openapi-to-postmanv2`), then enrich by hand — do not hand-author endpoints that the spec already describes, so the collection can be regenerated when the API changes. Document the regeneration command in `docs/postman/README.md` and add a `scripts/gen-postman.sh`.
- Folder structure mirroring the API: `Auth`, `Users`, `Items`, plus a `Smoke` folder holding the ordered end-to-end run.
- Environment variables only — **no hardcoded hosts, no committed secrets**: `baseUrl` (`http://localhost:4455` local, `https://…:4455` staging/prod), `email`, `password`, `accessToken`, `refreshToken`, `itemId`, `activationToken`, `resetToken`. Credentials in the committed environments are placeholders; real values come from a gitignored `*.local.postman_environment.json`.
- Scripts:
  - Post-response script on `login` and `refresh` capturing `accessToken` / `refreshToken` into environment variables; collection-level Bearer auth reading `{{accessToken}}` so no request sets the header manually.
  - Post-response script on `create item` capturing `{{itemId}}` for the subsequent GET/PUT/PATCH/DELETE.
  - `pm.test` assertions on every request: status code, response schema/required fields, and for error cases the `ProblemDetail` `code`.
- The `Smoke` folder must be **runnable start-to-finish with Newman in collection order**: register → activate → login → create item → list → get → update → delete → refresh → logout. Since activation and reset tokens arrive by email, document the two supported ways to supply them and implement one: reading them from MailHog's HTTP API in a pre-request script (preferred for local/staging), or a test-profile-only endpoint that is **never enabled in prod**.
- Include negative-path requests: login with wrong password, login before activation, protected endpoint without a token, expired/tampered token, reusing a rotated refresh token, resetting with a consumed token.
- Notes in `docs/postman/README.md` on TLS against the dev certificate trio: import the dev CA into Postman, or `newman run … --insecure` for throwaway local runs, with a warning never to use `--insecure` against staging or prod.
- Wire `newman run` into the CI pipeline as the **post-deploy smoke test** for staging (§9 stage 8), using CI secrets for credentials and publishing the Newman HTML report as an artifact. This replaces the ad-hoc smoke step, or extends it — say which in the ADR.

---

## 13. THINGS I WILL REJECT

Entities exposed over REST · `ddl-auto` anything but `validate` · plaintext or BCrypt-without-justification passwords · tokens stored in clear · H2 used to "integration test" Postgres behaviour · tests asserting only HTTP 200 · `catch (Exception e) { }` · secrets in `application.yml`, compose files or the image · a pipeline whose "test" stage skips integration tests · certificates copied into `src/main/resources` · a deploy step that asks me to run `openssl pkcs12 -export` by hand · serving only the leaf certificate and leaving clients to guess the intermediate · any endpoint that reveals whether an email is registered.
