# 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. |