From 273d412fb44716188e575b749476ed82ca313348 Mon Sep 17 00:00:00 2001 From: neodk2004 Date: Sat, 13 Jun 2026 10:25:30 +0200 Subject: [PATCH] docs: add README with setup, architecture, progress and debugging notes --- README.md | 313 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 313 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..0a9194c --- /dev/null +++ b/README.md @@ -0,0 +1,313 @@ +# PawFeed + +A social platform where pets are the stars. Dogs, cats, and birds get their own profiles — owners manage them, but the pet is the identity. Think Instagram built from the ground up for animals: pets follow each other, post photos and videos, celebrate milestones, and track their health, all within a community that actually cares about animals. + +**Core value:** Pets are first-class social identities — not just content on a human's feed — so every feature is built around the pet's life, not the owner's. + +--- + +## Tech Stack + +| Layer | Technology | Version | +|---|---|---| +| Framework | Next.js | 16.x | +| UI | React + Tailwind CSS v4 + shadcn/ui (base-nova preset) | React 19 | +| Language | TypeScript | 6.x | +| API | tRPC | 11.x | +| ORM | Prisma | 7.x | +| Database | PostgreSQL via Supabase (managed) | 16 | +| Auth | Clerk | 7.x | +| Media Storage | Supabase Storage (S3-compatible) | — | +| Cache / Feed | Upstash Redis (serverless) | — | +| Client Fetching | TanStack Query | 5.x | +| Tests | Vitest | 4.x | +| Hosting | Vercel (target) | — | + +**Design decisions:** +- All data access goes through **Prisma → direct Postgres connection**, never through Supabase PostgREST. RLS is enabled on all tables with no policies, which closes the PostgREST surface entirely. +- Media (photos, avatars, stories) is stored in **Supabase Storage** via presigned PUT URLs — file bytes never pass through the Next.js API server. +- The social feed uses a **Redis fan-out-on-write** pattern: on each new post, a sorted set entry is written into every follower's feed inbox. `feed.getFeed` reads from Redis and hydrates with Prisma. +- Authentication is handled by **Clerk** (owner accounts). Pet profiles are app-domain entities in Postgres, not Clerk users. + +--- + +## Project Structure + +``` +src/ + app/ + (app)/ # Protected app shell (Clerk middleware) + feed/ # Home feed page + StoryTray + pets/[petId]/ # Pet profile page + ProfileTabs + pets/[petId]/edit/ + pets/[petId]/followers/ + pets/[petId]/following/ + onboarding/ # Pet creation onboarding + (auth)/ # Clerk-rendered sign-in / sign-up + components/ + feed/ # PostCard, MilestoneCard, FeedList, SkeletonCard + layout/ # Sidebar, MobileNav + pet/ # AvatarUpload, PetForm, PetListItem + post-creation/ # PostTypeSheet, PhotoPostForm, StoryForm, MilestoneForm, MultiImageUpload + profile/ # ProfileTabs (Posts grid + Milestones list) + safety/ # BlockDialog, ReportSheet + stories/ # PawRing, StoryViewer, StoryTray, StoryForm + ui/ # shadcn component copies (button, dialog, sheet, etc.) + context/ + ActivePetContext.tsx # Global active pet state (pet switcher) + hooks/ + useActivePet.ts + lib/ + auth.ts # Clerk server helper + avatar-url.ts # CDN URL construction for avatars + feed-helpers.ts # fanOutPost, backfillOnFollow (Redis) + media-url.ts # CDN URL construction for post/story media + milestone-meta.ts # MilestoneType → Lucide icon + label map + milestone-thresholds.ts # Follower count thresholds for auto-milestones + prisma.ts # Prisma singleton with PgBouncer adapter + r2.ts # AWS SDK S3 client for Cloudflare R2 (Phase 5+) + redis.ts # Upstash Redis client + supabase-storage.ts # Supabase Storage presigned URL helpers (server-only) + utils.ts # cn() helper + trpc/ + init.ts # tRPC context (userId + prisma) + server.ts # Server-side caller for RSC + query-client.ts # TanStack Query client factory + routers/ + _app.ts # Root router + pets.ts # CRUD for pet profiles + posts.ts # Photo posts (create, updateCaption, delete, byPetId) + stories.ts # 24h stories (create, listActive, hasActiveStory, recordView, getViewers) + milestones.ts # Milestone posts (create, listByPet) + feed.ts # Home feed (getFeed via Redis + Prisma hydration) + follows.ts # Pet-to-pet follow graph + blocks.ts # Block a pet + reports.ts # Report a post or pet + media.ts # Avatar presigned upload (getPresignedUrl, confirmUpload) + __tests__/ + helpers/ + prisma-mock.ts # Typed mock PrismaClient factory + auth.test.ts + feed.test.ts + follows.test.ts + blocks.test.ts + reports.test.ts + posts.test.ts + stories.test.ts + milestones.test.ts + proxy.ts # Next.js middleware (Clerk auth guard) +prisma/ + schema.prisma # Full DB schema + seed.ts # Species + breed seed (Dog/Cat/Bird) +supabase/ + migrations/ + 20260613000000_enable_rls_all_tables.sql +.planning/ # GSD planning artefacts (phases, plans, summaries) +``` + +--- + +## Setup + +### Prerequisites + +- Node.js 20+ +- A [Supabase](https://supabase.com) project (PostgreSQL + Storage) +- A [Clerk](https://clerk.com) application +- An [Upstash](https://upstash.com) Redis database + +### 1. Clone and install + +```bash +git clone +cd pawfeed +npm install +``` + +### 2. Environment variables + +Copy `.env.example` to `.env.local` and fill in all values: + +```bash +cp .env.example .env.local +``` + +| Variable | Where to find it | +|---|---| +| `DATABASE_URL` | Supabase → Project Settings → Database → Connection string (pooled, port 6543) | +| `DIRECT_URL` | Supabase → Project Settings → Database → Connection string (direct, port 5432) | +| `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` | Clerk Dashboard → API Keys | +| `CLERK_SECRET_KEY` | Clerk Dashboard → API Keys | +| `NEXT_PUBLIC_SUPABASE_URL` | Supabase → Project Settings → API → Project URL | +| `SUPABASE_SERVICE_ROLE_KEY` | Supabase → Project Settings → API → service_role key | +| `UPSTASH_REDIS_REST_URL` | Upstash Console → Redis database → REST URL | +| `UPSTASH_REDIS_REST_TOKEN` | Upstash Console → Redis database → REST Token | + +> `SUPABASE_SERVICE_ROLE_KEY` is a server-only secret. It is imported exclusively in `server-only` modules and never exposed to the client. + +### 3. Database setup + +Push the Prisma schema to Supabase: + +```bash +npx prisma db push +``` + +Seed species and breeds: + +```bash +npx prisma db seed +``` + +Enable Row Level Security on all tables (run once in Supabase SQL Editor): + +```sql +-- File: supabase/migrations/20260613000000_enable_rls_all_tables.sql +-- Paste the contents of this file into the Supabase SQL Editor and run. +``` + +> This closes the PostgREST surface. Prisma (direct connection with service_role) is unaffected. + +### 4. Supabase Storage + +Create a public bucket named `pet-avatars` in Supabase Storage (Dashboard → Storage → New bucket). + +### 5. Run the dev server + +```bash +npm run dev +``` + +Open [http://localhost:3000](http://localhost:3000). The app redirects unauthenticated users to Clerk sign-in. + +--- + +## Key Commands + +| Command | What it does | +|---|---| +| `npm run dev` | Start Next.js dev server (Turbopack) | +| `npm run build` | Production build | +| `npm run lint` | ESLint | +| `npx vitest run` | Run full test suite | +| `npx vitest run src/__tests__/posts.test.ts` | Run a single test file | +| `npx tsc --noEmit` | Type check without emitting | +| `npx prisma db push` | Sync schema to DB (no migration files) | +| `npx prisma studio` | Open Prisma Studio (DB browser) | +| `npx prisma db seed` | Re-seed species + breeds | +| `npx prisma generate` | Regenerate Prisma client after schema change | + +--- + +## Architecture Notes + +### Identity model + +``` +Clerk User (owner) + └── owns 1..n Pet profiles (Postgres) + └── Pet is the social identity (posts, follows, stories, milestones) +``` + +An owner can switch between their pets via `ActivePetContext`. All tRPC mutations assert `pet.ownerId === ctx.userId` before writing. + +### Media pipeline + +``` +Client → tRPC getPresignedUrl → Supabase Storage signed URL +Client → XHR PUT directly to signed URL (file bytes never hit Next.js server) +Client → tRPC create/confirmUpload (writes storage key to DB) +Feed → getMediaUrl(storageKey) → Supabase public CDN URL +``` + +### Feed architecture + +``` +Post created + └── fanOutPost(redis, postId, petId, createdAt, prisma) + └── for each follower: ZADD feed:{followerPetId} score=timestamp member=postId + +feed.getFeed({ petId, cursor }) + └── ZRANGE feed:{petId} → postIds + └── prisma.post.findMany({ where: { id: { in: postIds } } }) → hydrated posts +``` + +### Story expiry (lazy-delete) + +Stories are never deleted in real time. All queries filter `expiresAt > NOW()`. A weekly cleanup job purges expired rows. Story views are retained 30 days for analytics. + +### PawRing (story indicator) + +Instead of a standard circular ring (Instagram-style), active stories are indicated by a paw-print SVG outline around the avatar: a circular main pad ring with four toe-bean circles above it, all in orange-500. + +--- + +## Development Progress + +### Phase 1 — Foundation (complete) + +- Next.js 16 + Tailwind v4 + shadcn/ui (base-nova) scaffold +- Clerk auth with middleware, protected app shell, onboarding flow +- Prisma 7 + Supabase PostgreSQL (pooled via PgBouncer) +- tRPC 11 + TanStack Query 5 wired end-to-end +- Upstash Redis client +- Supabase Storage presigned upload pipeline +- Pet CRUD (create, edit, avatar upload) +- ActivePetContext (pet switcher) +- Responsive layout (Sidebar + MobileNav) + +### Phase 2 — Content Core (complete) + +- **Social graph:** pet-to-pet follow/unfollow, followers/following pages +- **Safety:** block a pet, report a post or profile +- **Feed:** Redis fan-out-on-write, infinite scroll FeedList, PostCard +- **Photo posts:** 1–10 image carousel (Embla), caption edit/delete, presigned upload +- **Stories:** 24h photo stories, paw-print ring indicator, StoryTray, StoryViewer with progress bar + viewer list +- **Milestones:** 5 structured milestone types (Birthday, Adoption Day, First Outing, Vet Visit, New Litter), distinct orange card, PostTypeSheet fully wired for all 3 post types +- **Profile:** Posts 3-col grid + Milestones tab + +### Phase 3 — Engagement (planned) + +Likes, comments, reactions, notifications, explore/search. + +### Phase 4 — Growth (planned) + +Follower-milestone auto-trigger, push notifications, pet discovery. + +### Phase 5 — Video (planned) + +Short-form video stories and posts via Mux (HLS adaptive streaming). + +### Phase 6 — Messaging (planned) + +Direct messages between pet owners via Supabase Realtime. + +### Phase 7 — Health (planned) + +Vet records, vaccination logs, weight tracking. + +--- + +## Testing + +Tests are unit tests only (no DB, no network). Prisma and Redis are mocked per file using `createMockPrisma()` from `src/__tests__/helpers/prisma-mock.ts`. + +```bash +npx vitest run # full suite (~3-4s) +npx vitest # watch mode +``` + +> `src/__tests__/media-upload.test.ts` currently fails due to a missing Supabase URL in the test environment. This is a pre-existing issue from Phase 1 and does not affect any Phase 2 tests. + +--- + +## Known Issues / Debugging Notes + +| Issue | Notes | +|---|---| +| `DATABASE_URL` must use the **pooled** connection (port 6543, `?pgbouncer=true`) | Prisma uses this at runtime. `DIRECT_URL` (port 5432) is only for `prisma db push` / migrations. | +| Prisma on Windows requires `DIRECT_URL` set in `.env.local` | The Prisma CLI reads `.env.local` via a custom `prisma.config.ts`. If `npx prisma db push` hangs, check `DIRECT_URL` is set. | +| Supabase service_role key is never client-facing | All files that import `supabase-storage.ts` must be `server-only`. The client only ever receives signed URLs. | +| `asChild` prop is not available in the base-nova shadcn preset | Do not use `asChild` on any shadcn component. This affects `PopoverTrigger`, `DropdownMenuItem`, etc. Style the trigger element directly. | +| StoryView records are NOT cascade-deleted when a story expires | This is intentional (D-08): view analytics are retained 30 days. A separate cleanup job will purge old views. | +| Feed cleanup on post delete is best-effort in Phase 2 | When a post is deleted, its ID is not removed from Redis sorted sets. `feed.getFeed` silently skips IDs that Prisma doesn't return. Full cleanup (ZREM) is scheduled for Phase 4. |