Files

314 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 <repo-url>
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. |