docs: add README with setup, architecture, progress and debugging notes

This commit is contained in:
admin committed 2026-06-13 10:25:30 +02:00
1 parent 3fa62d2ce0
commit 273d412fb4
1 file changed
+313
+313
View File
@@ -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 <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. |