` โ Biome will flag the latter.
+
+10. **`@clerk/express` v1 API only:** Use `clerkMiddleware`, `getAuth`, `requireAuth` from `@clerk/express`. Do not use the deprecated `ClerkExpressRequireAuth` from v0.
diff --git a/.claude/prds/feat-002-validation.md b/.claude/prds/feat-002-validation.md
new file mode 100644
index 0000000..e2d0798
--- /dev/null
+++ b/.claude/prds/feat-002-validation.md
@@ -0,0 +1,82 @@
+# feat-002: Validation Report
+
+**Verdict:** PASS
+
+---
+
+## Summary
+
+The feat-002 technical spec and design spec are well-constructed and consistent with the PRD, research file, and all referenced authoritative specs (L2-002, L3-002, L3-008, L4-001). The hexagonal architecture is correctly applied, security invariants from L2-002 and L3-002 are honoured, and the design spec handles the Clerk appearance token-mapping exception correctly and explicitly. One minor gap and two warnings are noted but none are blocking.
+
+---
+
+## Checks Passed
+
+### Technical Spec
+
+- `clerk_id` never returned in API responses โ Section 7.3 explicitly states: "`clerk_id` is NEVER returned in any API response. It is an internal join key only." The `UserProfile` response shape omits it.
+- Parameterised queries in all SQL โ Section 5.3 `UserRepositoryPg` specifies `$1, $2` parameterised queries throughout, and Section 5.3 "Constraints" explicitly prohibits string interpolation. Migration SQL uses no dynamic construction.
+- Auth context extracted from middleware, never from request body โ Controller logic in Section 7.2 extracts `clerkUserId` from `authPort.getAuthContext(req)` (which calls `getAuth(req)` from Clerk middleware). No controller reads `user_id` from `req.body`.
+- `MOCK_AUTH=true` pattern allows tests without real Clerk โ `MockAuthAdapter` is fully specified (Section 5.2), selected in `server.ts` via `process.env.MOCK_AUTH === 'true'` (Section 8), and the integration test suite uses it (Section 10.2).
+- JWT validated by Clerk middleware before any route handler โ `clerkMiddleware()` runs globally before all routes; `requireAuth()` is applied to the entire `/v1` router group (Section 8). Health check is mounted before auth-protected routes.
+- User roles stored in local DB (not in Clerk JWT) โ Sections 1, 3.1, and the research file all explicitly state roles live in the local `users.roles` column (TEXT[]). The research file Section RBAC Design states: "Clerk JWT does NOT carry MMF roles".
+- `GET /v1/me` creates user on first call (lazy upsert pattern) โ `GetOrCreateUserService` (Section 6.1) implements the upsert-on-first-call pattern. The upsert SQL uses `ON CONFLICT (clerk_id) DO UPDATE` (Section 5.3).
+- Database migration has both `-- migrate:up` and `-- migrate:down` โ Section 2 migration has both sections. Down migration is `DROP TABLE IF EXISTS users`.
+- Migration uses `update_updated_at_column()` (already exists) โ Section 2 includes a comment: "update_updated_at_column() is defined in 20260305120000_add_updated_at_trigger.sql โ Do NOT redefine it here."
+- No string interpolation in SQL queries โ Section 5.3 explicitly: "Parameterised queries only โ no string interpolation in query construction."
+- Monetary values (N/A for this feature) โ No monetary values in scope for feat-002.
+- Acceptance criteria are testable โ Section 12 lists 21 discrete, binary-testable acceptance criteria covering authentication, CRUD endpoints, role guards, forbidden actions, and frontend behaviour.
+- Error responses use format `{ error: { code, message } }` โ Section 7.3 shows error format including `code`, `message`, and `correlation_id`. Section 12 AC confirms this. Consistent with L2-002 Section 5.3 requirement.
+
+### Design Spec
+
+- Clerk appearance uses raw values (not CSS vars) โ Section 2.1 uses raw hex/rgba throughout. Section 2.2 explicitly explains: "Clerk's `appearance` prop does not consume CSS custom properties โ it injects inline styles. This is the only place in the codebase where Tier 1 identity token values are used directly." The exception is documented with a full mapping table.
+- All page backgrounds use `--color-bg-page` or approved gradient โ Sign In/Sign Up pages use `--gradient-hero`; Loading screen uses `--color-bg-page` (`#060A14`). Both are approved tokens per the colour summary in Section 8.
+- Loading state uses `prefers-reduced-motion` safe pattern โ Section 5.2 includes `@media (prefers-reduced-motion: reduce)` that disables animation and sets static opacity. Correctly referenced against L2-001 Section 5.2.
+- Semantic HTML for loading state (`