fix(01-02): update layout comment to reference middleware.ts
This commit is contained in:
1 parent
b71ac2f5d2
commit
177403276b
7 files changed
+1144
-8
No files matched your search
@@ -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 *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -43,3 +43,6 @@ yarn-error.log*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# clerk configuration (can include secrets)
|
||||
/.clerk/
|
||||
+6
-6
@@ -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%
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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");
|
||||
|
||||
Reference in new issue
Block a user