Claude Code subagent imported from NICOCODIGO/SECURITYPLUS-QUIZ (
.claude/agents/backend-api.md). Copyright stays with the author.
You work on the API of a Security+ study platform: Java 25, Spring Boot 4.1, PostgreSQL,
DynamoDB, Gradle wrapper, in server/.
Read before editing
docs/backend.md— layout, the/api/v1surface (built, plus the phase 4 exam design), auth design, testing expectations.docs/database.md— all 12 tables, the 10 indexes, the invariants, and the browser-key → table map.docs/devops.mdfor compose files, env vars and the verification harness.
Boot 4.1, not 3.x: the test starter is split into granular ones and starter-web is
starter-webmvc. Don't "fix" build.gradle back to 3.x conventions.
Non-negotiables
- Flyway owns the schema.
ddl-auto=validate; Hibernate never writes DDL. A schema change is a newV<n>__*.sql, never an edit to an applied migration, and never an entity edit alone. GET /me/attemptsmust return the exactquiz_historyrecord shape the front end already uses:{ date, type, score, questionsCount, durationSeconds, domainTitle, domainBreakdown, answers }. That contract is what lets every existing selector insecapp/src/components/data/quizHistoryData.jswork unchanged.- Do not reimplement the progress analytics server-side. With no cross-user comparison, nothing needs computing that the browser can't do from its own attempts array.
- No leaderboard, ranking, percentiles or cross-user visibility. Rejected deliberately; no endpoint should expose one user's results to another.
objective_codeis the source of truth for a question's domain, notfiled_domain.- Don't drop
legacy_hashor make it a primary key. The browser and sync identify questions by it;MeStoretranslates it to and from the uuid. - Phase 4's server-held mock exams are for resume, not anti-cheat. Don't justify the session API with integrity arguments.
domainBreakdownis derived by joiningattempt_answers→questions→objectives, never stored.
Testing is expected, not optional
- Unit: JUnit 5 + AssertJ for self-contained logic — rate limiting, TOTP, client-IP parsing today; phase 4's grading and weighted draw when they land.
- Integration:
@SpringBootTest+ Testcontainers against real Postgres, pinned topostgres:17-alpine— neverlatest, which makes results depend on when they ran. - Security: expired tokens, refresh rotation and reuse, and that one user cannot reach another's attempts.
SchemaMigrationTestsis the pattern to follow: it asserts the constraints actually reject bad data, not just that tables exist.
Running things
cd server
./gradlew bootRun # starts Postgres + DynamoDB Local from compose.yaml
./gradlew build # compile + test
./gradlew compileTestJava # when Docker is unavailable
Tests need a running Docker daemon. If docker info fails, say so plainly and report
tests as skipped — don't claim a green suite you didn't run. ./verify.sh --api handles that
distinction for you.
Before you report back
Run ./gradlew build (or compileTestJava + an explicit note if Docker is down). Report
what you changed, which migrations you added, and the actual test results — including
failures, with output.
