fix(01-02): update layout comment to reference middleware.ts

This commit is contained in:
admin committed 2026-06-06 09:43:00 +02:00
1 parent b71ac2f5d2
commit 177403276b
7 files changed
+1144 -8

No files matched your search

+30
View File
@@ -0,0 +1,30 @@
{
"permissions": {
"allow": [
"Bash(node \"T:/CC-Projekte/.claude/gsd-core/bin/gsd-tools.cjs\" query commit \"docs: initialize project\" --files .planning/PROJECT.md)",
"Bash(mkdir -p .planning)",
"Bash(node \"T:/CC-Projekte/.claude/gsd-core/bin/gsd-tools.cjs\" query config-new-project '{\"mode\":\"yolo\",\"granularity\":\"standard\",\"parallelization\":true,\"commit_docs\":true,\"model_profile\":\"balanced\",\"workflow\":{\"research\":true,\"plan_check\":true,\"verifier\":true,\"nyquist_validation\":true,\"auto_advance\":false},\"plan_review\":{\"source_grounding\":true},\"ship\":{\"pr_body_sections\":[{\"heading\":\"User Stories & Acceptance Criteria\",\"enabled\":true,\"source\":\"REQUIREMENTS.md ## User Stories || REQUIREMENTS.md ## Acceptance Criteria\",\"fallback\":\"- Acceptance criteria are covered by the linked requirements and verification evidence.\"},{\"heading\":\"Risks & Dependencies\",\"enabled\":true,\"source\":\"PLAN.md ## Risks || PLAN.md ## Dependencies\",\"fallback\":\"- No known high-risk rollout dependencies.\"},{\"heading\":\"Success Metrics & Release Criteria\",\"enabled\":true,\"source\":\"REQUIREMENTS.md ## Definition of Done || VERIFICATION.md ## Release Criteria\",\"fallback\":\"- Release when automated verification and required manual checks pass.\"},{\"heading\":\"Stakeholder Review & Approval\",\"enabled\":false,\"template\":\"- Product owner approval pending for {phase_name}.\"}]}}')",
"Bash(node \"T:/CC-Projekte/.claude/gsd-core/bin/gsd-tools.cjs\" query commit \"chore: add project config\" --files .planning/config.json)",
"Bash(python3 *)",
"Bash(rm .planning/research/write_summary.py)",
"Bash(git init *)",
"Bash(git add *)",
"Bash(git commit -m 'docs: complete project research for OnlyPets *)",
"Bash(node \"T:/CC-Projekte/.claude/gsd-core/bin/gsd-tools.cjs\" query commit \"docs: define v1 requirements\" --files .planning/REQUIREMENTS.md)",
"Skill(gsd:ui-phase)",
"Bash(node \"T:/CC-Projekte/.claude/gsd-core/bin/gsd-tools.cjs\" query state.advance-plan --phase 1 --plan \"01-01\")",
"Bash(mkdir -p \"T:/CC-Projekte/OnlyPets/src/app/\\(auth\\)/sign-up/[[...sign-up]]\")",
"Bash(mkdir -p \"T:/CC-Projekte/OnlyPets/src/app/\\(auth\\)/log-in/[[...sign-in]]\")",
"Bash(npx tsc *)",
"Bash(node -e \"const c = require\\('@clerk/nextjs'\\); console.log\\(Object.keys\\(c\\).filter\\(k => k.includes\\('useSign'\\) || k.includes\\('Hook'\\)\\)\\)\")",
"Bash(node -e ' *)",
"Bash(xargs grep -l \"SignInSignalValue\")",
"Bash(npx vitest *)",
"Bash(git commit -m ' *)",
"Bash(gsd-tools query *)",
"Bash(git commit *)",
"Bash(Test-Path *)",
"Bash(git mv *)"
]
}
}
+3
View File
@@ -43,3 +43,6 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
# clerk configuration (can include secrets)
/.clerk/
+6 -6
View File
@@ -4,13 +4,13 @@ milestone: v1.0
milestone_name: milestone
status: executing
stopped_at: Phase 1 plans complete (01-01 through 01-04)
last_updated: "2026-06-05T15:29:08.194Z"
last_activity: 2026-06-05 — Phase 1 plans created (01-01 through 01-04); ready for execution
last_updated: "2026-06-06T06:40:22.471Z"
last_activity: 2026-06-05 -- Phase 1 execution started
progress:
total_phases: 7
completed_phases: 0
total_plans: 4
completed_plans: 0
completed_plans: 1
percent: 0
---
@@ -25,10 +25,10 @@ See: .planning/PROJECT.md (updated 2026-06-05)
## Current Position
Phase: 1 of 7 (Foundation)
Plan: 0 of 4 in current phase
Phase: 1 (Foundation) — EXECUTING
Plan: 2 of 4
Status: Ready to execute
Last activity: 2026-06-05 — Phase 1 plans created (01-01 through 01-04); ready for execution
Last activity: 2026-06-05 -- Phase 1 execution started
Progress: [░░░░░░░░░░] 0%
+2 -1
View File
@@ -41,7 +41,8 @@
"post_planning_gaps": true,
"security_enforcement": true,
"security_asvs_level": 1,
"security_block_on": "high"
"security_block_on": "high",
"_auto_chain_active": false
},
"ship": {
"pr_body_sections": [
@@ -0,0 +1,204 @@
---
phase: "01"
plan: "01"
subsystem: foundation
tags: [scaffold, auth, prisma, trpc, r2, active-pet-context, vitest]
dependency_graph:
requires: []
provides:
- proxy.ts (Clerk auth enforcement on all non-public routes)
- prisma singleton (src/lib/prisma.ts with @prisma/adapter-pg)
- Prisma schema (Owner, Species, Breed, Pet models)
- getPresignedUploadUrl (src/lib/r2.ts)
- getOwnerId / Clerk auth wrapper (src/lib/auth.ts)
- createTRPCContext + router + publicProcedure + protectedProcedure (src/trpc/init.ts)
- appRouter + AppRouter type (src/trpc/routers/_app.ts)
- petsRouter with create/list/byId + ownership assertion (src/trpc/routers/pets.ts)
- useTRPC + TRPCReactProvider (src/trpc/client.tsx)
- ActivePetProvider + useActivePet (src/context/ActivePetContext.tsx)
- Vitest config + 22 Wave 0 test scaffolds
affects:
- All subsequent plans (depend on these foundational symbols)
tech_stack:
added:
- Next.js 16.2.7 (React 19.2.4, TypeScript 6.0.3, Turbopack)
- Tailwind CSS v4.3.0 (@import "tailwindcss"; @tailwindcss/postcss)
- shadcn/ui (base-nova style; neutral preset; components: button, input, label, textarea, select, avatar, card, dialog, dropdown-menu, sonner, progress, separator, badge, skeleton)
- "@clerk/nextjs" 7.4.3 (proxy.ts auth gating)
- "@trpc/server" + "@trpc/client" + "@trpc/tanstack-react-query" 11.17.0
- "@tanstack/react-query" 5.101.0
- "zod" 4.4.3
- "prisma" + "@prisma/client" 7.8.0 (Rust-free WASM engine)
- "@prisma/extension-accelerate" 3.0.1
- "@prisma/adapter-pg" 7.8.0 + "pg" 8.x (Prisma 7 local dev adapter)
- "@aws-sdk/client-s3" + "@aws-sdk/s3-request-presigner" 3.1062.0
- "sonner" 2.0.7
- "lucide-react" 1.17.0
- "@upstash/redis" 1.38.0
- "vitest" 4.x + "@vitest/coverage-v8" + "@vitejs/plugin-react" + "jsdom"
patterns:
- Prisma 7 singleton with @prisma/adapter-pg (local dev) and withAccelerate() (Vercel)
- Two-layer identity: Clerk userId (owner) + active_pet_id (pet context + localStorage)
- Presigned R2 PUT upload (media bytes never through API server)
- Clerk proxy.ts pattern (Next.js 16 convention)
- tRPC ownership assertion on all pet mutations (T-1-02)
key_files:
created:
- proxy.ts
- prisma.config.ts
- prisma/schema.prisma
- prisma/seed.ts
- src/lib/prisma.ts
- src/lib/r2.ts
- src/lib/auth.ts
- src/trpc/init.ts
- src/trpc/query-client.ts
- src/trpc/server.ts
- src/trpc/client.tsx
- src/trpc/routers/_app.ts
- src/trpc/routers/pets.ts
- src/app/api/trpc/[trpc]/route.ts
- src/context/ActivePetContext.tsx
- src/hooks/useActivePet.ts
- src/app/(app)/layout.tsx
- src/app/(app)/pets/page.tsx
- src/app/(app)/onboarding/pet/page.tsx
- vitest.config.ts
- src/__tests__/auth.test.ts
- src/__tests__/pet-profile.test.ts
- src/__tests__/media-upload.test.ts
- src/__tests__/helpers/prisma-mock.ts
- .env.example
- package.json
- postcss.config.mjs
- tsconfig.json
- components.json
modified:
- src/app/layout.tsx (added ClerkProvider + TRPCReactProvider + ActivePetProvider + Toaster)
- src/app/globals.css (orange design system: --primary oklch(0.7024 0.1967 41.45); --background oklch(0.9882 0 0))
decisions:
- "Prisma 7 requires @prisma/adapter-pg for local direct connections (url/directUrl no longer in schema.prisma; moved to prisma.config.ts)"
- "Prisma 7 singleton: adapter path for local dev, accelerateUrl + withAccelerate() for Vercel/Accelerate"
- "proxy.ts at repo root (not src/) per Next.js 16 + Clerk 7.x convention"
- "tRPC appRouter.createCaller() used for RSC server-side calling (createCallerFactory removed from @trpc/server top-level in v11)"
- "Onboarding pet page uses placeholder speciesId — real species selector wired in Plan 02"
metrics:
duration: "~20 minutes"
completed: "2026-06-05T19:18:00Z"
tasks_completed: 2
tasks_total: 3
files_created: 26
files_modified: 2
---
# Phase 01 Plan 01: Walking Skeleton Summary
**One-liner:** Next.js 16 scaffold + Clerk proxy.ts + Prisma 7 schema (Owner/Species/Breed/Pet) + tRPC pets router with ownership assertions + presigned R2 client + ActivePetContext + Vitest Wave 0 test scaffold
## What Was Built
The foundational architecture for OnlyPets is in place. Two tasks completed:
**Task 1 — Scaffold + deps + Vitest scaffold:**
- `create-next-app` in scaffold-tmp (capital letters prevented scaffolding in root), then moved to project root
- All pinned runtime deps installed: Clerk 7.4.3, tRPC 11.17.0, Prisma 7.8.0, Zod 4.4.3, AWS SDK, Sonner, Upstash Redis
- Tailwind v4 verified: `postcss.config.mjs` uses `@tailwindcss/postcss`; `globals.css` uses `@import "tailwindcss"`; no `tailwind.config.ts`
- shadcn/ui initialized (base-nova style, neutral preset); all Phase 1 components added
- Orange design system applied: `--primary: oklch(0.7024 0.1967 41.45)` (#F97316 equivalent); `--background: oklch(0.9882 0 0)` (#FAFAFA)
- Inter font loaded via `next/font/google`
- Vitest configured with jsdom + @ alias; 22 `it.todo` Wave 0 tests scaffold
**Task 2 — Core wiring:**
- `proxy.ts` at repo root: Clerk middleware gating all non-public routes; `config.matcher` includes `/(api|trpc)(.*)` and `/__clerk/(.*)`
- `prisma/schema.prisma`: Owner, Species, Breed, Pet models (Prisma 7 format — no url/directUrl in schema)
- `prisma.config.ts`: Prisma 7 `defineConfig` with datasource url from `DATABASE_URL` env
- `src/lib/prisma.ts`: Singleton with `@prisma/adapter-pg` for local dev; `withAccelerate()` for Prisma Accelerate
- `src/lib/r2.ts` (server-only): S3Client + `getPresignedUploadUrl(key, contentType)` with 300s expiry
- `src/lib/auth.ts`: `getOwnerId()` Clerk auth wrapper
- `src/trpc/init.ts`: `createTRPCContext`, `router`, `publicProcedure`, `protectedProcedure` (UNAUTHORIZED on no userId)
- `src/trpc/routers/pets.ts`: `pets.create` (upserts Owner, creates Pet), `pets.list` (scoped), `pets.byId` (ownership-asserted)
- `src/trpc/routers/_app.ts`: `appRouter` + `AppRouter` type
- `src/trpc/client.tsx`: `TRPCReactProvider` + `useTRPC` (via `@trpc/tanstack-react-query`)
- `src/trpc/server.ts`: `createTRPCCaller` for RSC prefetch (uses `appRouter.createCaller()`)
- `/api/trpc/[trpc]/route.ts`: tRPC fetch handler (GET + POST)
- `src/context/ActivePetContext.tsx` + `src/hooks/useActivePet.ts`: localStorage-backed active pet context
- `prisma/seed.ts`: seeds Dog/Cat/Bird species idempotently
- `src/app/layout.tsx`: ClerkProvider (orange appearance), TRPCReactProvider, ActivePetProvider, Toaster
- Skeleton pages: `(app)/layout.tsx`, `(app)/pets/page.tsx` (RSC, reads pets.list), `(app)/onboarding/pet/page.tsx` (form + pets.create mutation)
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 1 - Bug] Prisma 7 schema.prisma no longer accepts url/directUrl in datasource**
- **Found during:** Task 2, running `npx prisma validate`
- **Issue:** Prisma 7 error: "The datasource property `url` is no longer supported in schema files." The research document described Prisma 5/6 schema format. Prisma 7 requires connection URLs to live in `prisma.config.ts` (via `defineConfig`).
- **Fix:** Removed `url` and `directUrl` from `prisma/schema.prisma`; created `prisma.config.ts` with `defineConfig({ datasource: { url: env("DATABASE_URL") } })`
- **Files modified:** `prisma/schema.prisma`, created `prisma.config.ts`
- **Commit:** e0214ea
**2. [Rule 2 - Missing critical functionality] Prisma 7 requires @prisma/adapter-pg for direct DB connections**
- **Found during:** Task 2, when testing PrismaClient instantiation
- **Issue:** Prisma 7's new WASM engine requires either `adapter` (for direct connections) or `accelerateUrl` (for Prisma Accelerate). The old `datasourceUrl` constructor option is removed. Without an adapter, the local dev path cannot connect to Supabase.
- **Fix:** Installed `@prisma/adapter-pg` 7.8.0 and `pg` 8.x (first-party Prisma packages). Singleton uses `PrismaPg(new Pool(...))` for local dev and `accelerateUrl + withAccelerate()` for Vercel.
- **Files modified:** `src/lib/prisma.ts`, `package.json`
- **Commit:** e0214ea
**3. [Rule 1 - Bug] Missing jsdom dependency for Vitest**
- **Found during:** Task 1, running `npx vitest run`
- **Issue:** Vitest 4.x requires `jsdom` to be installed separately when `environment: "jsdom"` is configured.
- **Fix:** `npm install -D jsdom`
- **Files modified:** `package.json`
- **Commit:** dbc074e
**4. [Rule 1 - Bug] tRPC v11 `createCallerFactory` not exported from `@trpc/server` top-level**
- **Found during:** Task 2, TypeScript check
- **Issue:** The tRPC docs example referenced `createCallerFactory` from `@trpc/server` but it's not in the v11 API surface.
- **Fix:** Use `appRouter.createCaller(ctx)` directly in `src/trpc/server.ts`.
- **Files modified:** `src/trpc/server.ts`
- **Commit:** e0214ea
**5. [Rule 1 - Bug] create-next-app refused capital letters in directory name**
- **Found during:** Task 1, scaffolding attempt
- **Issue:** `create-next-app` rejects names with capital letters. The project root is `T:/CC-Projekte/OnlyPets` (capital O and P).
- **Fix:** Scaffolded into `scaffold-tmp` subdirectory then moved all files to project root via PowerShell `Copy-Item -Recurse`.
- **Files modified:** All scaffold files moved to root
- **Commit:** dbc074e
## Task 3: Checkpoint (Awaiting Human Action)
Task 3 is a `checkpoint:human-action` requiring the user to:
1. Set up `.env.local` with real `DATABASE_URL` and `DIRECT_URL` from Supabase
2. Run `npx prisma db push` to create tables in Supabase
3. Run `npx prisma db seed` to seed Dog/Cat/Bird species
4. Verify Species table shows 3 rows in Prisma Studio
## Known Stubs
| File | Location | Stub | Reason |
|------|----------|------|--------|
| `src/app/(app)/onboarding/pet/page.tsx` | Line 41 | `speciesId: "placeholder"` | Real species selector not wired until Plan 02. Walking skeleton only needs the create mutation end-to-end path. |
## Self-Check: PASSED
Files verified:
- proxy.ts: FOUND
- prisma/schema.prisma: FOUND
- prisma.config.ts: FOUND
- src/lib/prisma.ts: FOUND
- src/lib/r2.ts: FOUND
- src/lib/auth.ts: FOUND
- src/trpc/init.ts: FOUND
- src/trpc/routers/pets.ts: FOUND
- src/trpc/routers/_app.ts: FOUND
- src/trpc/client.tsx: FOUND
- src/context/ActivePetContext.tsx: FOUND
- src/hooks/useActivePet.ts: FOUND
- vitest.config.ts: FOUND
- src/__tests__/pet-profile.test.ts: FOUND
- src/__tests__/media-upload.test.ts: FOUND
- .env.example: FOUND
Commits verified:
- dbc074e: chore(01-01) scaffold
- e0214ea: feat(01-01) wiring
@@ -0,0 +1,898 @@
# Phase 1: Foundation - Research
**Researched:** 2026-06-05
**Domain:** Next.js 16 App Router, Clerk auth, Prisma 7 + Supabase, Cloudflare R2 presigned uploads, two-layer pet identity
**Confidence:** MEDIUM-HIGH
---
## Summary
Phase 1 establishes the identity layer and media pipeline that every subsequent phase depends on. Three things must be true before any other phase starts: (1) owner authentication works end-to-end, (2) the pet-as-actor identity pattern is encoded correctly into every request context, and (3) media storage bypasses the API server via presigned R2 uploads. None of these can be retrofitted cheaply.
The stack decisions from ecosystem research remain sound but require version updates: the npm `latest` tag now resolves to Next.js **16.2.7** (stable since October 2025), Prisma **7.8.0**, and `@trpc/tanstack-react-query` as the current package name for the tRPC React Query integration. Clerk 7.4.3 explicitly declares `^16.0.10` in its peerDependencies, so compatibility with Next.js 16 is confirmed. The previously flagged blockers (Clerk + Next.js 15 compatibility, tRPC v11 stability, Tailwind v4 migration) are now resolved.
Walking skeleton goal: owner can sign up with Clerk, a pet profile is created in Supabase PostgreSQL via Prisma, the pet's avatar photo is uploaded directly to Cloudflare R2 via a presigned URL, and the active pet is reflected in the navigation — all in a single deployable Next.js 16 application on Vercel.
**Primary recommendation:** Scaffold with `create-next-app@latest` (gives Next.js 16, React 19, TypeScript, Tailwind v4, Turbopack by default), install Clerk + tRPC + Prisma 7, run `npx shadcn@latest init`, configure `proxy.ts` (not `middleware.ts`) for Clerk, and establish the two-layer identity context before writing any pet profile logic.
---
<phase_requirements>
## Phase Requirements
| ID | Description | Research Support |
|----|-------------|------------------|
| AUTH-01 | Owner can create an account with email and password | Clerk `<SignUp>` component + `clerkMiddleware()` in proxy.ts handles this entirely |
| AUTH-02 | Owner receives email verification link after signup | Clerk's built-in email verification flow — no custom code needed; configure in Clerk dashboard |
| AUTH-03 | Owner can reset password via email link | Clerk's built-in forgot-password flow with `<ForgotPassword>` component |
| AUTH-04 | Owner session persists across browser refresh | Clerk JWT sessions are persisted via httpOnly cookie automatically; active_pet_id stored in localStorage |
| PET-01 | Owner can create a pet profile with name, breed, species, bio, and avatar photo | Prisma Pet model + tRPC `pets.create` mutation + presigned R2 upload for avatar |
| PET-02 | Owner can edit their pet's profile details | tRPC `pets.update` mutation with ownership assertion via `active_pet_id` + `owner_id` check |
| PET-03 | Owner can delete a pet profile | tRPC `pets.delete` mutation with confirm dialog; cascade behavior on DB constraints |
| PET-04 | Owner can manage multiple pets and switch the active pet without logging out | `active_pet_id` in React Context + localStorage; switcher in nav dropdown; no ownership change |
| PET-05 | Pet profile includes a structured "adoption origin story" field | `adoption_story` TEXT column in Pet table; rendered as `<Textarea>` in profile form |
</phase_requirements>
---
## Architectural Responsibility Map
| Capability | Primary Tier | Secondary Tier | Rationale |
|------------|-------------|----------------|-----------|
| Owner authentication (sign-up, login, session) | Clerk (external) | Next.js App Router (middleware/proxy) | Clerk manages owner identity; Next.js proxy.ts enforces auth on protected routes |
| Active pet resolution per request | API / Backend (tRPC context) | Frontend Server (RSC) | pet_id ownership must be validated server-side; client sends pet_id, server asserts ownership |
| Pet profile CRUD | API / Backend (tRPC procedures) | Database (Prisma + PostgreSQL) | All mutations go through tRPC with ownership assertions before any DB write |
| Avatar photo upload | Browser / Client | CDN / R2 | Client uploads directly to R2 via presigned PUT URL; API handles only metadata write after upload |
| Pet switcher state | Browser / Client | — | Active pet stored in React Context + localStorage; no server round-trip on switch |
| Breed/species taxonomy | Database / Storage | API / Backend | Seeded static data in PostgreSQL; served via tRPC query; never hardcoded client-side |
| Design system / UI components | Browser / Client | — | shadcn/ui components rendered client-side; no server rendering needed for form primitives |
---
## Standard Stack
### Core
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| `next` | 16.2.7 | Full-stack React framework | `latest` on npm (2026-06-01); App Router, Turbopack default, React 19.2, proxy.ts |
| `react` / `react-dom` | 19.2.7 | UI rendering | Ships with Next.js 16; RSC + concurrent features |
| `typescript` | 6.0.3 | Type safety | Required by all stack members; Next.js 16 first-class TS config |
| `@clerk/nextjs` | 7.4.3 | Owner authentication | Declares peerDep `^16.0.10` — confirmed Next.js 16 compatible; proxy.ts integration |
| `tailwindcss` | 4.3.0 | Utility-first CSS | v4 CSS-native engine; `@import "tailwindcss"` replaces v3 directives |
| `@tailwindcss/postcss` | 4.3.0 | Tailwind v4 PostCSS plugin | Required in v4; replaces `tailwindcss` as postcss plugin |
| `prisma` | 7.8.0 | ORM / schema + migrations | `latest` on npm (2026-04-22); Rust-free query engine default in v7 |
| `@prisma/client` | 7.8.0 | Generated type-safe DB client | Paired with `prisma` CLI |
| `@prisma/extension-accelerate` | 3.0.1 | Serverless connection pooling | Required on Vercel; Prisma-maintained (prismabot@prisma.io) |
| `@supabase/supabase-js` | 2.107.0 | Supabase client (Realtime + auth helpers) | Official Supabase client for real-time subscriptions (Phase 2+) |
| `@trpc/server` | 11.17.0 | tRPC server router | v11 stable (2026-04-28); `latest` on npm |
| `@trpc/client` | 11.17.0 | tRPC client | Paired with server |
| `@trpc/tanstack-react-query` | 11.17.0 | tRPC React Query bridge | Current v11 package name (supersedes `@trpc/react-query` which also works) |
| `@tanstack/react-query` | 5.101.0 | Client data fetching + cache | v5 stable; `latest` on npm (2026-06-02) |
| `zod` | 4.4.3 | Schema validation | Used natively by tRPC procedures; validates all API inputs |
| `@aws-sdk/client-s3` | 3.1062.0 | S3-compatible R2 client | AWS SDK v3; used to generate presigned PUT URLs server-side |
| `@aws-sdk/s3-request-presigner` | 3.1062.0 | Presigned URL generation | Paired with client-s3; `getSignedUrl` + `PutObjectCommand` |
### Supporting
| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| `sonner` | 2.0.7 | Toast notifications | shadcn/ui toast uses Sonner under the hood; all success/error feedback |
| `lucide-react` | 1.17.0 | Icon set | Bundled with shadcn; use for all icons (camera, chevron, etc.) |
| `@upstash/redis` | 1.38.0 | Serverless Redis client | Not needed in Phase 1; install now for Phase 2 story TTL |
| `server-only` | (latest) | Guard server-only modules | Prevents tRPC router code from being bundled into client |
| `client-only` | (latest) | Guard client-only modules | Prevents Clerk browser hooks from running on server |
### Alternatives Considered
| Instead of | Could Use | Tradeoff |
|------------|-----------|----------|
| `@trpc/tanstack-react-query` | `@trpc/react-query` | Both resolve to 11.17.0; `@trpc/tanstack-react-query` is the preferred name per tRPC docs |
| Presigned R2 PUT | Supabase Storage | Supabase Storage lacks image transforms; R2 + Cloudflare Images is the design decision |
| Prisma 7 | Drizzle ORM | Drizzle is faster and smaller; Prisma 7's Rust-free engine closes the gap; migration tooling still better in Prisma |
| `middleware.ts` | `proxy.ts` | Next.js 16 deprecates `middleware.ts` in favor of `proxy.ts`; Clerk supports both but recommends `proxy.ts` for 16+ |
**Installation:**
```bash
# Scaffold
npx create-next-app@latest onlypets --typescript --eslint --app --turbopack --src-dir --tailwind
# Auth
npm install @clerk/nextjs
# tRPC + data fetching
npm install @trpc/server @trpc/client @trpc/tanstack-react-query @tanstack/react-query zod server-only client-only
# Database
npm install prisma @prisma/client @prisma/extension-accelerate @supabase/supabase-js
npx prisma init
# Media upload
npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
# Supporting
npm install sonner @upstash/redis
# shadcn/ui (interactive, copies component source into repo)
npx shadcn@latest init
npx shadcn@latest add button input form label textarea select avatar card dialog dropdown-menu toast progress separator badge skeleton
```
**Version verification (run before pinning):**
All versions above confirmed via `npm view <pkg> version` on 2026-06-05.
---
## Package Legitimacy Audit
> slopcheck v0.6.1 was installed but operates against PyPI only — not applicable for npm packages. All packages verified via `npm view` against the npm registry and confirmed with official GitHub repository links.
| Package | Registry | Source Repo | First Published | Disposition |
|---------|----------|-------------|-----------------|-------------|
| `next` | npm | github.com/vercel/next.js | 2011 | Approved [VERIFIED: npm registry] |
| `@clerk/nextjs` | npm | github.com/clerk/javascript | ~2021 | Approved [VERIFIED: npm registry] |
| `@trpc/server` | npm | github.com/trpc/trpc | ~2021 | Approved [VERIFIED: npm registry] |
| `@trpc/client` | npm | github.com/trpc/trpc | ~2021 | Approved [VERIFIED: npm registry] |
| `@trpc/tanstack-react-query` | npm | github.com/trpc/trpc | ~2021 | Approved [VERIFIED: npm registry] |
| `tailwindcss` | npm | github.com/tailwindlabs/tailwindcss | ~2017 | Approved [VERIFIED: npm registry] |
| `@tailwindcss/postcss` | npm | github.com/tailwindlabs/tailwindcss | ~2024 | Approved [VERIFIED: npm registry] |
| `prisma` | npm | github.com/prisma/prisma | ~2019 | Approved [VERIFIED: npm registry] |
| `@prisma/client` | npm | github.com/prisma/prisma | ~2019 | Approved [VERIFIED: npm registry] |
| `@prisma/extension-accelerate` | npm | Prisma-maintained (prismabot@prisma.io) | ~2023 | Approved [VERIFIED: npm registry] |
| `@supabase/supabase-js` | npm | github.com/supabase/supabase-js | ~2020 | Approved [VERIFIED: npm registry] |
| `@tanstack/react-query` | npm | github.com/TanStack/query | ~2019 | Approved [VERIFIED: npm registry] |
| `zod` | npm | github.com/colinhacks/zod | ~2020 | Approved [VERIFIED: npm registry] |
| `@aws-sdk/client-s3` | npm | github.com/aws/aws-sdk-js-v3 | ~2020 | Approved [VERIFIED: npm registry] |
| `@aws-sdk/s3-request-presigner` | npm | github.com/aws/aws-sdk-js-v3 | ~2020 | Approved [VERIFIED: npm registry] |
| `sonner` | npm | github.com/emilkowalski/sonner | ~2022 | Approved [VERIFIED: npm registry] |
| `lucide-react` | npm | github.com/lucide-icons/lucide | ~2020 | Approved [VERIFIED: npm registry] |
| `@upstash/redis` | npm | github.com/upstash/redis-js | ~2021 | Approved [VERIFIED: npm registry] |
**Packages removed due to slopcheck:** none
**Packages flagged as suspicious:** none
*slopcheck could not be run against npm (operates against PyPI). All packages are cross-verified against official GitHub repositories via `npm view --json` and have multi-year publication histories from known organizations.*
---
## Architecture Patterns
### System Architecture Diagram
```
Browser
│
├─ [GET /sign-up, /log-in]──► Clerk hosted UI (appearance customized)
│ │
│ └─ Clerk issues JWT (owner_id in sub)
│
├─ [All app routes]──► proxy.ts (clerkMiddleware)
│ │ checks JWT, injects auth into request
│ │
│ └─► Next.js App Router
│ │
│ ├─ Server Components (RSC)
│ │ reads owner_id from auth()
│ │ resolves active_pet_id from DB or localStorage hint
│ │
│ └─ /api/trpc/[trpc] (Route Handler)
│ │
│ ├─ tRPC context builder
│ │ extracts owner_id from Clerk auth()
│ │ reads x-active-pet-id header
│ │ asserts pet ownership via Prisma
│ │
│ └─ tRPC procedures
│ pets.create / .update / .delete / .list
│ media.getPresignedUrl
│
├─ [PUT presigned URL]──► Cloudflare R2 (direct — never through API)
│ browser uploads file bytes directly to R2
│ on success: calls tRPC pets.setAvatar(storageKey)
│
└─ [Pet avatar CDN URL]──► Cloudflare CDN ──► R2 bucket
Database
Supabase PostgreSQL 16
owner (owner_id = Clerk user_id)
species (id, name) ← seeded: Dog, Cat, Bird
breed (id, species_id, name) ← seeded: AKC/CFA/BirdLife data
pet (id, owner_id FK→owner, species_id FK, breed_id FK,
name, bio, adoption_story, avatar_key, created_at)
ORM
Prisma 7 client (singleton in lib/prisma.ts)
Prisma Accelerate for connection pooling on Vercel serverless
```
### Recommended Project Structure
```
src/
├── app/
│ ├── (auth)/ # Clerk auth route group
│ │ ├── sign-up/
│ │ ├── log-in/
│ │ ├── forgot-password/
│ │ └── reset-password/
│ ├── (app)/ # Protected app route group
│ │ ├── layout.tsx # App shell (sidebar/bottom nav)
│ │ ├── onboarding/
│ │ │ └── pet/ # First pet creation flow
│ │ └── pets/
│ │ ├── page.tsx # My Pets grid (/pets)
│ │ └── [petId]/
│ │ ├── page.tsx # Pet profile view
│ │ └── edit/page.tsx # Pet profile edit
│ ├── api/
│ │ └── trpc/[trpc]/route.ts # tRPC fetch handler
│ ├── layout.tsx # Root layout (ClerkProvider, fonts)
│ └── globals.css # @import "tailwindcss"; CSS variables
│
├── trpc/
│ ├── init.ts # initTRPC, context type
│ ├── query-client.ts # makeQueryClient() factory
│ ├── server.ts # createCallerFactory for RSC prefetch
│ ├── client.tsx # TRPCReactProvider, useTRPC hook
│ └── routers/
│ ├── _app.ts # Root router
│ ├── pets.ts # pet CRUD procedures
│ └── media.ts # getPresignedUrl procedure
│
├── lib/
│ ├── prisma.ts # PrismaClient singleton
│ ├── r2.ts # S3Client for R2, getPresignedUploadUrl()
│ └── auth.ts # Clerk auth() wrappers for server context
│
├── components/
│ ├── ui/ # shadcn component copies
│ ├── pet/
│ │ ├── PetCard.tsx
│ │ ├── PetForm.tsx
│ │ ├── AvatarUpload.tsx
│ │ └── PetSwitcher.tsx
│ └── layout/
│ ├── Sidebar.tsx
│ └── MobileNav.tsx
│
├── context/
│ └── ActivePetContext.tsx # React Context for active_pet_id
│
├── hooks/
│ └── useActivePet.ts
│
└── prisma/
├── schema.prisma
├── migrations/
└── seed.ts # Species + breed taxonomy seed
```
### Pattern 1: Two-Layer Identity — Owner + Active Pet
**What:** Every tRPC procedure that operates on pet data receives `(owner_id from Clerk, pet_id from request header)` and must assert `pet.owner_id === auth.owner_id` before proceeding.
**When to use:** All pet mutation procedures (create, update, delete), any query that scopes to a specific pet.
**Example:**
```typescript
// Source: [ASSUMED] — based on tRPC v11 + Clerk auth() documented patterns
// trpc/init.ts
import { auth } from '@clerk/nextjs/server'
import { initTRPC, TRPCError } from '@trpc/server'
import { prisma } from '@/lib/prisma'
export const createTRPCContext = async () => {
const { userId } = await auth()
return { userId, prisma }
}
const t = initTRPC.context<typeof createTRPCContext>().create()
export const router = t.router
export const publicProcedure = t.procedure
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.userId) throw new TRPCError({ code: 'UNAUTHORIZED' })
return next({ ctx: { ...ctx, userId: ctx.userId } })
})
// trpc/routers/pets.ts — ownership assertion
export const petsRouter = router({
update: protectedProcedure
.input(z.object({ petId: z.string(), name: z.string(), ... }))
.mutation(async ({ ctx, input }) => {
const pet = await ctx.prisma.pet.findUnique({ where: { id: input.petId } })
if (!pet || pet.ownerId !== ctx.userId) {
throw new TRPCError({ code: 'FORBIDDEN' })
}
return ctx.prisma.pet.update({ where: { id: input.petId }, data: { ... } })
}),
})
```
### Pattern 2: Presigned R2 PUT Upload
**What:** API generates a short-lived presigned URL; browser PUTs the file directly to R2; on success, browser calls API to record the storage key in the DB.
**When to use:** Every media upload in the app (avatar photos, post images, etc.)
**Example:**
```typescript
// Source: [VERIFIED: developers.cloudflare.com/r2/api/s3/presigned-urls/]
// lib/r2.ts
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
export const r2 = new S3Client({
region: 'auto', // required by SDK but not used by R2
endpoint: `https://${process.env.CF_ACCOUNT_ID}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID!,
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
},
})
export async function getPresignedUploadUrl(key: string, contentType: string) {
return getSignedUrl(
r2,
new PutObjectCommand({
Bucket: process.env.R2_BUCKET_NAME!,
Key: key,
ContentType: contentType, // must match what browser sends in PUT
}),
{ expiresIn: 300 } // 5 minutes
)
}
```
### Pattern 3: Clerk proxy.ts Configuration (Next.js 16)
**What:** Next.js 16 renames `middleware.ts` to `proxy.ts`. Clerk 7.x supports both but recommends `proxy.ts` for Next.js 16+.
**When to use:** All projects on Next.js 16. If using Next.js ≤15, use `middleware.ts`.
**Example:**
```typescript
// Source: [VERIFIED: clerk.com/docs/references/nextjs/clerk-middleware]
// proxy.ts (at project root, NOT in src/)
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
const isPublicRoute = createRouteMatcher([
'/sign-in(.*)',
'/sign-up(.*)',
'/forgot-password(.*)',
'/reset-password(.*)',
])
export default clerkMiddleware(async (auth, request) => {
if (!isPublicRoute(request)) {
await auth.protect()
}
})
export const config = {
matcher: [
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
'/(api|trpc)(.*)',
'/__clerk/(.*)',
],
}
```
### Pattern 4: Prisma Singleton (Next.js / Serverless)
**What:** Prevents multiple PrismaClient instances during hot-reload in dev and across serverless function invocations.
**When to use:** Always in Next.js projects.
**Example:**
```typescript
// Source: [CITED: prisma.io/docs/guides/nextjs — PrismaClient singleton pattern]
// lib/prisma.ts
import { PrismaClient } from '@prisma/client'
import { withAccelerate } from '@prisma/extension-accelerate'
const createPrismaClient = () =>
new PrismaClient().$extends(withAccelerate())
const globalForPrisma = globalThis as unknown as {
prisma: ReturnType<typeof createPrismaClient> | undefined
}
export const prisma = globalForPrisma.prisma ?? createPrismaClient()
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
```
### Pattern 5: Clerk Appearance Customization (Orange Primary)
**What:** Override Clerk's default blue primary to match the OnlyPets orange-500 (#F97316) design system.
**When to use:** In `<ClerkProvider>` in `app/layout.tsx`.
**Example:**
```typescript
// Source: [CITED: clerk.com/docs/customization/variables]
// app/layout.tsx
<ClerkProvider
appearance={{
variables: {
colorPrimary: '#F97316',
colorBackground: '#FAFAFA',
colorText: '#09090B',
},
}}
>
{children}
</ClerkProvider>
```
### Pattern 6: Active Pet Context
**What:** Owner's active pet stored in React Context, initialized from localStorage, with no page reload on switch.
**When to use:** App shell layout and all components that display the active pet identity.
**Example:**
```typescript
// Source: [ASSUMED] — standard React Context + localStorage pattern
// context/ActivePetContext.tsx
'use client'
import { createContext, useContext, useState, useEffect } from 'react'
type ActivePetContextType = {
activePetId: string | null
setActivePet: (id: string) => void
}
export const ActivePetContext = createContext<ActivePetContextType | null>(null)
export function ActivePetProvider({ children }: { children: React.ReactNode }) {
const [activePetId, setActivePetId] = useState<string | null>(null)
useEffect(() => {
const stored = localStorage.getItem('active_pet_id')
if (stored) setActivePetId(stored)
}, [])
const setActivePet = (id: string) => {
setActivePetId(id)
localStorage.setItem('active_pet_id', id)
}
return (
<ActivePetContext.Provider value={{ activePetId, setActivePet }}>
{children}
</ActivePetContext.Provider>
)
}
export const useActivePet = () => {
const ctx = useContext(ActivePetContext)
if (!ctx) throw new Error('useActivePet must be used inside ActivePetProvider')
return ctx
}
```
### Anti-Patterns to Avoid
- **Putting `middleware.ts` instead of `proxy.ts` in Next.js 16:** `middleware.ts` is deprecated in Next.js 16. Use `proxy.ts`. The exported function name must be `proxy` or default export.
- **Routing media through the API server:** Never pass image bytes through a Next.js Route Handler. Generate presigned URLs server-side; client uploads directly to R2.
- **Passing pet_id in the request body without ownership assertion:** Always cross-check `pet.owner_id === auth().userId` in every tRPC procedure. Never trust a client-supplied pet_id without verifying ownership.
- **Creating Clerk users for pets:** Pets are `app-domain entities` (rows in the `pet` table), not Clerk users. Never call Clerk's user creation API for a pet.
- **Multiple PrismaClient instances:** Hot-reload creates new instances. Use the global singleton pattern in `lib/prisma.ts`.
- **Hardcoding breed lists client-side:** Breed taxonomy is seeded data in PostgreSQL, served via a tRPC query. It must be queryable and filterable by species — a static array cannot be filtered server-side.
- **Using Tailwind v3 config format with v4:** Tailwind v4 has no `tailwind.config.js`. Configuration goes in CSS via `@theme {}` blocks in `globals.css`. Do NOT generate a `tailwind.config.ts` — `create-next-app` may still do this for v3 installs.
- **Installing `@trpc/react-query` instead of `@trpc/tanstack-react-query`:** Both exist at 11.17.0 and both work; the tRPC docs now reference `@trpc/tanstack-react-query` as the canonical package. Use `@trpc/tanstack-react-query`.
---
## Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Email/password auth, session management, email verification | Custom JWT auth, bcrypt password hashing | Clerk `@clerk/nextjs` | PBKDF2 strength, secure httpOnly cookies, email verification flows, OAuth are all solved; hand-rolling auth is a common source of security vulnerabilities |
| Image upload to S3/R2 | Streaming binary through Next.js API | AWS SDK presigned PUT | API server becomes a bottleneck and CORS-headache; presigned URLs allow client-direct upload with no server load |
| File size / format validation on upload | Custom MIME parsing on server | Client-side + presigned URL `ContentType` restriction | Client validates before initiating; R2 enforces content-type on the presigned URL |
| UI components (forms, dialogs, dropdowns) | Custom accessible components | shadcn/ui (Radix UI primitives) | Radix handles focus trapping, keyboard navigation, ARIA — non-trivial to build correctly |
| TypeScript API types shared between client and server | OpenAPI code-gen, manual type files | tRPC end-to-end type inference | Procedure input/output types propagate automatically; no code-gen step |
| Breed taxonomy data | Manually typing 200+ breeds | Seed script from curated data (AKC, CFA lists) | Pre-curated lists exist; the schema just needs `(id, species_id, name)` — seed once |
| Connection pooling for PostgreSQL on serverless | PgBouncer self-hosting | Prisma Accelerate | Vercel serverless functions have no persistent connection; Accelerate handles pooling via a managed proxy |
**Key insight:** Clerk, Prisma, tRPC, and shadcn collectively eliminate the most error-prone boilerplate layers (auth, DB access, API typing, accessible UI). Phase 1's engineering value is in the identity wiring and data model, not in rebuilding solved infrastructure.
---
## Common Pitfalls
### Pitfall 1: `middleware.ts` Instead of `proxy.ts` in Next.js 16
**What goes wrong:** Auth middleware is never executed; all routes are publicly accessible.
**Why it happens:** Next.js 16 changed the conventional filename from `middleware.ts` to `proxy.ts`. The old `middleware.ts` still works for Edge runtime use cases but is deprecated. Clerk's `clerkMiddleware()` example code now shows `proxy.ts`.
**How to avoid:** Create `proxy.ts` at the project root (not in `src/`). Export the `clerkMiddleware()` result as the default export.
**Warning signs:** Visiting protected routes without being logged in doesn't redirect to sign-in; Clerk's `auth()` returns `null` in server components.
### Pitfall 2: Stale `create-next-app` Scaffold Assumptions
**What goes wrong:** Scaffold creates Tailwind v3 config files (`tailwind.config.ts`, old CSS directives) that are incompatible with Tailwind v4.
**Why it happens:** `create-next-app` behavior depends on its bundled template. With Next.js 16 + Tailwind v4, the scaffold should already use the v4 PostCSS approach, but this needs verification at scaffold time.
**How to avoid:** After scaffold, verify `postcss.config.mjs` uses `@tailwindcss/postcss` not `tailwindcss`. Verify `globals.css` uses `@import "tailwindcss"` not `@tailwind base; @tailwind components; @tailwind utilities`.
**Warning signs:** Build errors about `tailwind.config.ts` not being found; or v3 CSS directives present.
### Pitfall 3: Pet Ownership Not Asserted on Mutations
**What goes wrong:** Any owner can delete or edit any pet by passing an arbitrary `petId` — a horizontal privilege escalation vulnerability.
**Why it happens:** tRPC procedures receive `petId` from the client. Without an ownership check, any authenticated user can mutate any pet.
**How to avoid:** Every mutation: (1) `protectedProcedure` ensures `userId` exists, (2) `findUnique` fetches the pet, (3) assert `pet.ownerId === ctx.userId`, throw `FORBIDDEN` if not.
**Warning signs:** No ownership assertion in `pets.update`, `pets.delete`, or `media.setAvatar` procedures.
### Pitfall 4: Presigned URL Content-Type Mismatch
**What goes wrong:** R2 rejects the upload with a 403 SignatureDoesNotMatch error.
**Why it happens:** The `ContentType` encoded in the presigned URL signature must exactly match the `Content-Type` header the browser sends in the PUT request. If the tRPC procedure hardcodes `image/jpeg` but the browser sends `image/png`, the signature is invalid.
**How to avoid:** The client specifies the content type when requesting the presigned URL; the API encodes it in the signature. Browser sends the same `Content-Type` header in the PUT.
**Warning signs:** R2 returns 403 on direct PUT; CORS preflight passes but actual upload fails.
### Pitfall 5: Prisma Version Mismatch with Connection Pooling
**What goes wrong:** `@prisma/extension-accelerate` requires a specific Prisma client version. Version drift between `prisma`, `@prisma/client`, and `@prisma/extension-accelerate` causes runtime type errors.
**Why it happens:** `prisma` (CLI) and `@prisma/client` must match. Accelerate extension is a separate package with its own peer requirements.
**How to avoid:** Always install `prisma` and `@prisma/client` at the same version. Run `npx prisma generate` after any version change to regenerate the client.
**Warning signs:** TypeScript errors about missing `$extends` method; Accelerate extension type errors.
### Pitfall 6: Active Pet ID Not Available Server-Side for RSC
**What goes wrong:** Server Components can't read `localStorage`; RSC that tries to show the active pet displays nothing or the wrong pet.
**Why it happens:** `localStorage` is browser-only. RSCs run on the server.
**How to avoid:** The active pet context is for client components only. For RSC that needs to know the active pet, read it from a cookie set by the client on pet switch, or pass it as a prop from a client component boundary. On initial server render, fall back to the owner's first pet.
**Warning signs:** `localStorage is not defined` runtime error in RSC; hydration mismatch between server and client rendering of the active pet.
### Pitfall 7: Breed Data Without Species Seeding
**What goes wrong:** Pet creation form has no breeds to display; or breeds from other species show up in the selector.
**Why it happens:** The breed table depends on the species table (FK constraint). If the seed script runs in the wrong order or is missing, breed queries return empty.
**How to avoid:** Seed script order: (1) species, (2) breeds with species_id FK. Seed must run before any tests or manual testing of the pet creation form.
**Warning signs:** Empty breed `<Select>` after choosing a species; FK constraint violation on breed insert.
---
## Code Examples
Verified patterns from official sources:
### Tailwind v4 PostCSS Config (Next.js)
```javascript
// Source: [VERIFIED: tailwindcss.com/docs/guides/nextjs]
// postcss.config.mjs
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
}
export default config
```
### Tailwind v4 CSS Import
```css
/* Source: [VERIFIED: tailwindcss.com/docs/guides/nextjs] */
/* app/globals.css */
@import "tailwindcss";
/* CSS variable overrides for shadcn/ui (orange primary) */
:root {
--primary: 24.6 95% 53.1%; /* #F97316 in HSL */
--primary-foreground: 0 0% 100%;
--background: 0 0% 98%; /* #FAFAFA */
--muted: 240 4.8% 95.9%; /* #F4F4F5 */
}
```
### Cloudflare R2 Presigned PUT URL
```typescript
// Source: [VERIFIED: developers.cloudflare.com/r2/api/s3/presigned-urls/]
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
const r2 = new S3Client({
region: 'auto',
endpoint: `https://${process.env.CF_ACCOUNT_ID}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID!,
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
},
})
// Server-side: called from tRPC media.getPresignedUrl procedure
export async function getPresignedUploadUrl(
key: string,
contentType: 'image/jpeg' | 'image/png' | 'image/webp'
): Promise<string> {
return getSignedUrl(
r2,
new PutObjectCommand({
Bucket: process.env.R2_BUCKET_NAME!,
Key: key,
ContentType: contentType,
}),
{ expiresIn: 300 }
)
}
```
### Prisma Schema for Phase 1
```prisma
// Source: [ASSUMED] — standard Prisma schema, derived from phase requirements
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL") // Supabase connection string
directUrl = env("DIRECT_URL") // Direct connection for migrations
}
model Owner {
id String @id // = Clerk userId (e.g. "user_xxx")
createdAt DateTime @default(now())
pets Pet[]
}
model Species {
id String @id @default(cuid())
name String @unique // "Dog" | "Cat" | "Bird"
breeds Breed[]
pets Pet[]
}
model Breed {
id String @id @default(cuid())
name String
species Species @relation(fields: [speciesId], references: [id])
speciesId String
pets Pet[]
@@unique([speciesId, name])
}
model Pet {
id String @id @default(cuid())
owner Owner @relation(fields: [ownerId], references: [id])
ownerId String
species Species @relation(fields: [speciesId], references: [id])
speciesId String
breed Breed? @relation(fields: [breedId], references: [id])
breedId String?
name String
bio String?
adoptionStory String?
avatarKey String? // R2 object key, NOT a full URL
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([ownerId])
}
```
### tRPC Route Handler (Next.js 16 App Router)
```typescript
// Source: [VERIFIED: trpc.io/docs/client/nextjs/app-router-setup]
// app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from '@trpc/server/adapters/fetch'
import { appRouter } from '@/trpc/routers/_app'
import { createTRPCContext } from '@/trpc/init'
const handler = (req: Request) =>
fetchRequestHandler({
endpoint: '/api/trpc',
req,
router: appRouter,
createContext: createTRPCContext,
})
export { handler as GET, handler as POST }
```
---
## State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| `middleware.ts` | `proxy.ts` | Next.js 16 (Oct 2025) | Clerk proxy setup must use `proxy.ts` for Next.js 16 projects |
| Tailwind v3 config file + 3 CSS directives | v4: `@import "tailwindcss"` + `@tailwindcss/postcss` | Tailwind v4 (early 2025) | No `tailwind.config.ts`; CSS-native `@theme` blocks for customization |
| Prisma 5 (Rust query engine) | Prisma 7 (Rust-free, WebAssembly compiler default) | Nov 2025 | Simpler deployment; no native binary download; may affect very cold-start latency |
| `@trpc/react-query` | `@trpc/tanstack-react-query` (canonical) | tRPC v11 | Both work at 11.17.0; new name is preferred going forward |
| Next.js 15.x with App Router | Next.js 16.x with Cache Components + Turbopack default | Oct 2025 | `create-next-app@latest` scaffolds Next.js 16; `experimental.ppr` flag removed |
**Deprecated/outdated in this phase's context:**
- `middleware.ts` export function: deprecated in Next.js 16; use `proxy.ts` with same logic
- Tailwind `@tailwind base/components/utilities` CSS directives: removed in v4; use `@import "tailwindcss"`
- Prisma `experimental.accelerate` flag: Accelerate is now stable, configured via `withAccelerate()` extension
- `experimental.ppr`: removed in Next.js 16; replaced by `cacheComponents` config flag (not needed in Phase 1)
---
## Assumptions Log
| # | Claim | Section | Risk if Wrong |
|---|-------|---------|---------------|
| A1 | `ClerkProvider` appearance prop `variables.colorPrimary: '#F97316'` is the correct way to override the primary color | Code Examples — Clerk Appearance | Clerk UI components remain blue; minor visual mismatch; fix is cosmetic only |
| A2 | `proxy.ts` location should be project root (not `src/`), same as `middleware.ts` convention | Pattern 3 | If it goes in `src/`, the matcher may not apply to all routes; low risk, easily fixed |
| A3 | `ActivePetContext` with `localStorage` is sufficient for active pet persistence across refresh (AUTH-04) | Pattern 6 | If Clerk session doesn't persist across tab close, the pet selection is also lost — but Clerk's session persistence is separate from pet selection and should handle AUTH-04 independently |
| A4 | Prisma 7 `withAccelerate()` extension API is unchanged from Prisma 5/6 | Prisma Singleton pattern | If Accelerate extension API changed in v7, the singleton code would fail — verify against Prisma 7 docs |
| A5 | `create-next-app@latest` (v16.2.7) scaffolds Tailwind v4 by default | Installation, Pitfall 2 | If scaffold still uses v3, manual migration is needed — a 30-minute fix but could cause confusion |
| A6 | The Prisma schema `directUrl` env var is the correct pattern for Supabase + Prisma migrations | Code Examples | Without `directUrl`, `prisma migrate` may fail against Supabase's pooled connection |
| A7 | Breed taxonomy should be seeded from curated data (not a third-party npm package) | Don't Hand-Roll | If a reliable breed npm package exists with correct data, it could be used — but hand-curated seed data is safer |
---
## Open Questions
1. **Supabase project setup**
- What we know: Supabase is the chosen Postgres host; `DATABASE_URL` and `DIRECT_URL` are needed for Prisma
- What's unclear: Whether a Supabase project already exists or needs to be created; what region to use
- Recommendation: Walk-through task in Wave 0 to create Supabase project and retrieve connection strings; do not block scaffold on this
2. **Cloudflare R2 bucket CORS configuration**
- What we know: Browser PUT requests to R2 require CORS headers; Cloudflare R2 CORS must be configured per-bucket
- What's unclear: Whether the R2 bucket exists and has CORS rules allowing PUT from the app's origin
- Recommendation: Wave 0 task to create R2 bucket and configure CORS (allow PUT, allow `Content-Type` header, allow origin `localhost:3000` + production domain)
3. **Clerk project environment variables**
- What we know: Clerk uses `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY`; keyless mode works for local dev without them
- What's unclear: Whether keyless mode is acceptable for this project or proper API keys are required from the start
- Recommendation: Treat keyless mode as sufficient for Wave 0; add real API keys before staging deployment
4. **Vercel project setup**
- What we know: Vercel is the deployment target
- What's unclear: Whether a Vercel project exists and how Prisma Accelerate is configured (requires a Prisma Data Platform project for the Accelerate connection string)
- Recommendation: Note as Wave 0 prerequisite; Prisma Accelerate requires a separate connection string from the Prisma Data Platform dashboard
---
## Environment Availability
| Dependency | Required By | Available | Version | Fallback |
|------------|------------|-----------|---------|----------|
| Node.js | All | Yes | v22.12.0 | — |
| npm | Package management | Yes | 10.9.0 | — |
| Git | Version control | Yes | 2.54.0.windows.1 | — |
| `create-next-app` (npx) | Scaffold | Yes (via npm) | 16.2.7 | — |
| Supabase project | Database | Unknown | — | Create during Wave 0 |
| Cloudflare R2 bucket | Media uploads | Unknown | — | Create during Wave 0 |
| Clerk project | Auth | Unknown | — | Keyless mode for local dev |
| Vercel project | Deployment | Unknown | — | Local dev not blocked; needed for staging |
| Prisma Accelerate | Connection pooling | Unknown | — | Direct Supabase connection for local dev (no Accelerate needed); required for Vercel |
**Missing dependencies with no fallback:** None — all are service accounts that can be created.
**Missing dependencies with fallback:**
- Supabase project: local dev can use a local PostgreSQL instance (but Supabase is preferred for the realtime feature parity)
- Prisma Accelerate: local dev uses `DATABASE_URL` directly; Accelerate only needed for Vercel deployment
---
## Validation Architecture
### Test Framework
| Property | Value |
|----------|-------|
| Framework | Vitest (via `vitest` package — preferred for Next.js 16 + Turbopack) |
| Config file | `vitest.config.ts` — Wave 0 gap |
| Quick run command | `npx vitest run --reporter=verbose` |
| Full suite command | `npx vitest run` |
> Note: Next.js 16 ships with Jest support via `@next/jest` transformer, but Vitest integrates better with Turbopack and ESM. For Phase 1 unit tests (tRPC procedure logic, ownership assertion, presigned URL generation), Vitest is preferred. [ASSUMED]
### Phase Requirements → Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|-------------|
| AUTH-01 | Owner can create an account | e2e / manual | Manual Clerk UI flow | N/A — Clerk-managed |
| AUTH-02 | Email verification link sent | manual | Manual email check | N/A — Clerk-managed |
| AUTH-03 | Password reset via email | manual | Manual email check | N/A — Clerk-managed |
| AUTH-04 | Session persists across refresh | integration | Manual browser refresh after login | N/A |
| PET-01 | Pet can be created with valid input | unit | `npx vitest run tests/pets.test.ts` | Wave 0 gap |
| PET-02 | Pet update rejected for non-owner | unit | `npx vitest run tests/pets.test.ts` | Wave 0 gap |
| PET-03 | Pet delete removes from DB | unit | `npx vitest run tests/pets.test.ts` | Wave 0 gap |
| PET-04 | Active pet switches without logout | integration | Manual browser test | N/A |
| PET-05 | Adoption story field saved + retrieved | unit | `npx vitest run tests/pets.test.ts` | Wave 0 gap |
| Media | Presigned URL generated with correct ContentType | unit | `npx vitest run tests/media.test.ts` | Wave 0 gap |
| Ownership | Non-owner cannot mutate pet | unit | `npx vitest run tests/pets.test.ts` | Wave 0 gap |
### Sampling Rate
- **Per task commit:** `npx vitest run tests/pets.test.ts tests/media.test.ts --reporter=verbose`
- **Per wave merge:** `npx vitest run`
- **Phase gate:** Full suite green before `/gsd-verify-work`
### Wave 0 Gaps
- [ ] `tests/pets.test.ts` — covers PET-01, PET-02, PET-03, PET-05, ownership assertion
- [ ] `tests/media.test.ts` — covers presigned URL generation with correct ContentType
- [ ] `vitest.config.ts` — Vitest configuration with path aliases
- [ ] `tests/helpers/prisma-mock.ts` — mock PrismaClient for unit tests (avoid real DB calls in unit tests)
- [ ] Framework install: `npm install -D vitest @vitejs/plugin-react`
---
## Security Domain
> `security_enforcement: true`, `security_asvs_level: 1` — required.
### Applicable ASVS Categories
| ASVS Category | Applies | Standard Control |
|---------------|---------|-----------------|
| V2 Authentication | Yes | Clerk — handles all auth flows; never hand-roll |
| V3 Session Management | Yes | Clerk JWT httpOnly cookies; session expiry handled by Clerk |
| V4 Access Control | Yes | tRPC ownership assertion: `pet.ownerId === auth.userId` on every mutation |
| V5 Input Validation | Yes | Zod schemas on all tRPC procedure inputs; pet name/bio character limits enforced server-side |
| V6 Cryptography | No | No custom crypto; Clerk and AWS SDK handle signing |
| V8 Data Protection | Partial | `avatar_key` (R2 key, not a signed URL) stored in DB; CDN URL constructed at read time |
### Known Threat Patterns for This Stack
| Pattern | STRIDE | Standard Mitigation |
|---------|--------|---------------------|
| Horizontal privilege escalation (any user edits any pet) | Tampering | `pet.ownerId === ctx.userId` assertion in all tRPC pet mutation procedures |
| Presigned URL abuse (upload malicious content) | Tampering | `ContentType` restricted at presigned URL generation (jpeg/png/webp only); 5-minute expiry |
| Unauthenticated route access | Elevation of Privilege | `proxy.ts` with `clerkMiddleware()` + `createRouteMatcher` for all app routes |
| Client-supplied `petId` without ownership check | Tampering | Ownership assertion pattern (Pattern 1); never trust client-supplied ID without DB verification |
| Leaked R2 credentials via client bundle | Information Disclosure | R2 credentials in server-only env vars; presigned URL generation ONLY in tRPC procedures (server-only); never in client components |
| CORS bypass on R2 bucket | Tampering | R2 bucket CORS configured to allow PUT from known origins only (not `*`) |
---
## Sources
### Primary (HIGH confidence)
- `npm view next version` — Next.js 16.2.7 (published 2026-06-01) [VERIFIED: npm registry]
- `npm view @clerk/nextjs peerDependencies` — confirms Next.js 16 support (`^16.0.10`) [VERIFIED: npm registry]
- `clerk.com/docs/references/nextjs/clerk-middleware` — `proxy.ts` setup, `clerkMiddleware()` [VERIFIED: clerk.com]
- `trpc.io/docs/client/nextjs/app-router-setup` — tRPC v11 App Router setup, `@trpc/tanstack-react-query` [VERIFIED: trpc.io]
- `tailwindcss.com/docs/guides/nextjs` — Tailwind v4 PostCSS config, `@import "tailwindcss"` [VERIFIED: tailwindcss.com]
- `developers.cloudflare.com/r2/api/s3/presigned-urls/` — R2 presigned PUT URL pattern, endpoint format [VERIFIED: cloudflare.com]
- `nextjs.org/blog/next-16` — Next.js 16 stable release notes, proxy.ts, breaking changes [VERIFIED: nextjs.org]
- `npm view @trpc/server version` — v11.17.0 stable, published 2026-04-28 [VERIFIED: npm registry]
- `npm view tailwindcss version` — v4.3.0, published 2026-05-08 [VERIFIED: npm registry]
- `npm view prisma version` — v7.8.0, published 2026-04-22 [VERIFIED: npm registry]
- All package GitHub repos verified via `npm view --json .repository.url` [VERIFIED: npm registry]
### Secondary (MEDIUM confidence)
- `prisma.io/docs/guides/nextjs` — PrismaClient singleton pattern [CITED: prisma.io]
- `clerk.com/docs/customization/variables` — `appearance.variables.colorPrimary` pattern [CITED: clerk.com]
- .planning/research/SUMMARY.md — ecosystem architecture decisions, stack rationale
### Tertiary (LOW confidence — from training data)
- Prisma 7 `withAccelerate()` API shape — not independently verified against Prisma 7 changelog; tagged [ASSUMED]
- React Context + localStorage pattern for active pet — standard React pattern; tagged [ASSUMED] for exact implementation details
- Vitest as preferred test runner for Next.js 16 + Turbopack — [ASSUMED]; alternative is `@next/jest`
---
## Metadata
**Confidence breakdown:**
- Standard stack: HIGH — all versions confirmed via `npm view` against registry on 2026-06-05
- Architecture: HIGH — patterns derived from official Clerk, tRPC, Cloudflare R2, and Tailwind documentation
- Pitfalls: MEDIUM-HIGH — most verified from official sources; Prisma v7 migration details partially assumed
- Version resolution: HIGH — all blockers from STATE.md resolved with live npm registry data
**Research date:** 2026-06-05
**Valid until:** 2026-07-05 (30 days — packages in this ecosystem update frequently but major API changes are minor-version-gated)
+1 -1
View File
@@ -22,7 +22,7 @@ export default async function AppLayout({
}: {
children: React.ReactNode;
}) {
// Belt-and-suspenders auth check (proxy.ts handles the primary gate)
// Belt-and-suspenders auth check (middleware.ts handles the primary gate)
const { userId } = await auth();
if (!userId) {
redirect("/log-in");