admin f899ed5f7c feat(landing): translate landing page copy (Landing i18n namespace)
The landing page was hardcoded English, so switching languages had no
visible effect on it — added a "Landing" namespace to both message
catalogs and wired src/app/page.tsx up via getTranslations, matching
the pattern already used by other server-rendered pages.
2026-08-11 20:09:13 +02:00

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
Video Mux (upload, transcoding, HLS streaming) —
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. Falls back to Postgres when Redis is unavailable.
  • Authentication is handled by Clerk (owner accounts). Pet profiles are app-domain entities in Postgres, not Clerk users.
  • Short-form video is handled by Mux: client uploads directly to a signed Mux URL → Mux transcodes and serves HLS via mux-player-react. The getByPostId query self-heals in dev by polling the Mux API directly, so webhooks are not required during local development.
  • Health data is strictly owner-private. The only publicly accessible health surface is the shareable health card (token URL with expiry), designed for vets, friends, and pet sitters. The emergency vet contact is intentionally excluded from the shared card — it lives only in the private dashboard.

Project Structure

src/
  app/
    (app)/              # Protected app shell (Clerk middleware)
      feed/             # Home feed page + StoryTray
      notifications/    # Notification center page
      pets/[petId]/     # Pet profile page + ProfileTabs
      pets/[petId]/edit/
      pets/[petId]/followers/
      pets/[petId]/following/
      pets/[petId]/health/  # Owner-only health dashboard (weight, vet visits, vaccines, emergency vet)
      search/           # Pet + People + Hashtag search
      messages/         # DM inbox list
      messages/[conversationId]/  # Conversation view
      onboarding/       # Pet creation onboarding
    (auth)/             # Clerk-rendered sign-in / sign-up
    health-card/[token]/  # Public shareable health card (no login required)
    api/
      trpc/             # tRPC HTTP handler
      cron/
        anniversaries/  # Vercel cron: fires birthday + adoption-day notifications daily at 08:00
      webhooks/
        mux/            # Mux webhook handler (video.asset.ready etc.)
  components/
    feed/               # PostCard, MilestoneCard, RepostCard, FeedList, SkeletonCard
                        # PawButton (paw reaction), PawBackButton (repost), CommentSheet
                        # WelcomeCard (new-user onboarding card, localStorage-dismissed)
    health/             # HealthDashboard (tabs), WeightChart (SVG bezier), PrintButton, AutoPrint
                        # EmergencyVetSection (Nominatim search + OSM map, owner-only)
    layout/             # Sidebar (with NotificationBell), MobileNav
    notifications/      # NotificationBell (badge + count)
    pet/                # AvatarUpload, PetForm, PetListItem
    post-creation/      # PostTypeSheet, PhotoPostForm, StoryForm, MilestoneForm,
                        # MultiImageUpload, VideoUploadForm (+ Mux direct upload)
    profile/            # ProfileTabs (Posts grid + Milestones list) — post detail as centered Dialog
    safety/             # BlockDialog, ReportSheet
    social/             # FollowButton
    stories/            # PawRing, StoryViewer, StoryTray, StoryForm
    ui/                 # shadcn component copies (button, dialog, sheet, etc.)
    video/              # VideoCard (polls PROCESSING -> plays READY via MuxPlayer)
  context/
    ActivePetContext.tsx # Global active pet state (pet switcher)
  lib/
    avatar-url.ts       # CDN URL construction for avatars
    feed-helpers.ts     # fanOutPost, backfillOnFollow (Redis)
    hashtag-helpers.ts  # extractHashtags, saveHashtags
    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
    mux.ts              # Mux Node SDK singleton + MUX_WEBHOOK_SECRET
    parse-caption.ts    # Tokenises captions into text + #hashtag segments
    prisma.ts           # Prisma singleton with PgBouncer adapter
    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 (birthday + adoptedAt fields included)
      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 with mode: "chrono" | "algo")
      follows.ts        # Pet-to-pet follow graph
      reactions.ts      # Paw reactions (toggle, count per post)
      comments.ts       # Flat comments (create, delete, listByPost)
      reposts.ts        # Paw-Back repost with attribution
      explore.ts        # Explore page (species/breed filter)
      search.ts         # Pet name/breed search + owner username search + hashtag search
      notifications.ts  # In-app notifications (list, unreadCount, markRead, markAllRead)
      videos.ts         # Mux video (createUpload, getByPostId with Mux self-heal, updateCaption)
      blocks.ts         # Block a pet
      reports.ts        # Report a post or pet
      media.ts          # Avatar presigned upload (getPresignedUrl, confirmUpload)
      messages.ts       # DM conversations (create, list, send, markRead)
      health.ts         # Weight logs, vet visits, vaccines, emergency vet, shareable health card
  __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
  middleware.ts         # Next.js middleware (Clerk auth guard)
prisma/
  schema.prisma         # Full DB schema — see Schema section below
  seed.ts               # Species + breed seed (Dog/Cat/Bird)
supabase/
  migrations/
    20260613000000_enable_rls_all_tables.sql
vercel.json             # Vercel cron config (anniversaries at 08:00 daily)
.planning/              # GSD planning artefacts (phases, plans, summaries)

Prisma Schema — Model Overview

Owner          — Clerk userId, owns n Pets
Pet            — Social identity. Fields: name, bio, adoptionStory, avatarKey,
                 dmPolicy, birthday (Date), adoptedAt (Date), species, breed
Post           — PHOTO | MILESTONE | REPOST | VIDEO
PostImage      — image storage keys (0-based position for carousel)
Milestone      — linked 1:1 to a Post (MilestoneType enum)
Story          — 24h ephemeral photos; expiresAt indexed
StoryView      — viewer tracking; retained 30 days
Follow         — Pet → Pet; (followerPetId, followeePetId) PK
Block          — Pet → Pet
Report         — Post or Pet report with ReportReason enum
Reaction       — 1 per (post, pet); unique constraint enforces toggle
Comment        — flat, 500-char VarChar
Repost         — PostType.REPOST; references originalPost
Hashtag        — unique tag string
PostHashtag    — Post ↔ Hashtag join
VideoPost      — Mux upload/asset/playback IDs + VideoStatus
Conversation   — Pet ↔ Pet DM pair; petAId always < petBId
Message        — body + readAt + senderPetId
WeightLog      — date (Date), weightGrams (Int), notes
VetVisit       — date (Date), reason, vetName, notes
Vaccine        — name, dateGiven, nextDueDate (Date, optional), notes
EmergencyVet   — 1:1 with Pet (unique petId); name*, address, phone, website, lat, lng
HealthCard     — 1:1 with Pet; token (uuid), expiresAt (nullable)
Notification   — type: FOLLOW|REACTION|COMMENT|MESSAGE|BIRTHDAY|ADOPTION_DAY

Setup

Prerequisites

  • Node.js 20+
  • A Supabase project (PostgreSQL + Storage)
  • A Clerk application
  • An Upstash Redis database
  • A Mux account (for video posts)

1. Clone and install

git clone <repo-url>
cd pawfeed
npm install

2. Environment variables

Copy .env.example to .env.local and fill in all values:

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
MUX_TOKEN_ID Mux Dashboard -> Settings -> API Access Tokens
MUX_TOKEN_SECRET Mux Dashboard -> Settings -> API Access Tokens
MUX_WEBHOOK_SECRET Mux Dashboard -> Settings -> Webhooks -> Signing secret
CRON_SECRET Generate a random string; paste same value in Vercel env vars

SUPABASE_SERVICE_ROLE_KEY and MUX_TOKEN_SECRET are server-only secrets — never exposed to the client.

3. Database setup

Push the Prisma schema to Supabase:

npx prisma db push
npx prisma generate

Seed species and breeds:

npx prisma db seed

Enable Row Level Security on all tables (run once in Supabase SQL Editor):

-- 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. Mux webhook (production only)

In Mux Dashboard -> Settings -> Webhooks, add an endpoint pointing to https://your-domain.com/api/webhooks/mux. Subscribe to:

  • video.upload.asset_created
  • video.asset.ready
  • video.asset.errored

In local development, the webhook is not needed. videos.getByPostId polls the Mux API directly when the video is still PROCESSING, so the status self-heals without a tunnel.

6. Vercel cron (production only)

vercel.json is already configured. In Vercel project settings, add the CRON_SECRET environment variable (same value as in .env.local). The cron fires daily at 08:00 UTC at /api/cron/anniversaries.

7. Run the dev server

npm run dev

Open 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 generate Regenerate Prisma client after schema change
npx prisma studio Open Prisma Studio (DB browser)
npx prisma db seed Re-seed species + breeds

Architecture Notes

Identity model

Clerk User (owner)
  └── owns 1..n Pet profiles (Postgres)
        └── Pet is the social identity (posts, follows, stories, milestones,
            reactions, comments, reposts, notifications, videos, DMs, health data)

An owner can switch between their pets via ActivePetContext. All tRPC mutations assert pet.ownerId === ctx.userId before writing.

Media pipeline — Photos

Client -> tRPC getPresignedUrl -> Supabase Storage signed URL
Client -> XHR PUT directly to signed URL (bytes never hit Next.js server)
Client -> tRPC create/confirmUpload (writes storage key to DB)
Feed -> getMediaUrl(storageKey) -> Supabase public CDN URL

Media pipeline — Video (Mux)

Client -> tRPC videos.createUpload -> Mux direct upload URL + PROCESSING Post row
Client -> XHR PUT directly to Mux URL (bytes never hit Next.js server)
Mux -> transcodes -> fires webhook -> /api/webhooks/mux -> updates VideoPost (READY + playbackId)
VideoCard polls videos.getByPostId every 5s -> transitions from spinner to MuxPlayer

In dev (no webhook): getByPostId checks the Mux API directly when the status is PROCESSING and updates the DB on the fly. No ngrok required.

Feed architecture

Post created
  └── fanOutPost(redis, postId, petId, createdAt, prisma)
        └── for each follower: ZADD feed:{followerPetId} score=timestamp member=postId

feed.getFeed({ petId, cursor, mode: "chrono" | "algo" })
  └── ZRANGE feed:{petId} -> postIds
        └── prisma.post.findMany({ where: { id: { in: postIds } } }) -> hydrated posts
  └── chrono mode: preserves Redis sorted-set order (newest first)
  └── algo mode:  client-side re-sort by algoScore()
        algoScore = reactions×3 + comments×5 + originalReposts×4 + recency bonus
        recency bonus: +10 (<2h), +5 (<6h), +2 (<24h)
  └── Fallback (Redis unavailable): Postgres query on follows + own posts

Feed toggle (FeedList)

FeedList shows a pill toggle at the top: Für dich (Sparkles, algo) / Aktuell (Clock, chrono). The preference is persisted to localStorage under pf_feed_mode. The mode is passed to trpc.feed.getFeed.infiniteQueryOptions({ mode }).

Notification triggers (fire-and-forget)

follows.follow      -> FOLLOW notification to followeePet
reactions.toggle    -> REACTION notification to post owner (skip if own post)
comments.create     -> COMMENT notification to post owner (skip if own post)
/api/cron/anniversaries (daily 08:00 UTC)
  -> BIRTHDAY notification if EXTRACT(MONTH/DAY FROM birthday) = today AND birthday <= NOW()-1year
  -> ADOPTION_DAY notification if EXTRACT(MONTH/DAY FROM adoptedAt) = today AND adoptedAt <= NOW()-1year

All in-app triggers are wrapped in .catch(() => {}) — they never crash the parent mutation. Anniversary notifications require 1+ year minimum before the first trigger.

Health data privacy

All health data (weight logs, vet visits, vaccines, emergency vet) is owner-private:

  • /pets/[petId]/health redirects any non-owner to the public profile
  • All tRPC health procedures call assertOwner() before any DB access
  • The only public surface is the HealthCard at /health-card/[token]
    • Token is a UUID stored in the HealthCard table
    • Expiry is user-selected: 7 / 14 / 30 / 60 / 90 days; checked server-side before rendering
    • Shows: vaccines, weight trend chart, vet visits
    • Does NOT show: emergency vet (private contact data stays off the shared card)
    • PDF export: ?print=1 auto-triggers window.print() after 600ms via AutoPrint client component
    • print:hidden Tailwind variant hides nav chrome during print

EmergencyVet (private, owner-only)

Stored in EmergencyVet table (1:1 with Pet). Managed via EmergencyVetSection.tsx inside HealthDashboard. Features:

  • Nominatim search (OpenStreetMap geocoding, free, no API key): debounced 500ms, Accept-Language: de, dropdown with address suggestions
  • OSM iframe map: openstreetmap.org/export/embed.html?bbox=...&marker=lat,lng
  • Manual fields: name (required), address, phone, website
  • Clicking a result fills coords + address; manual override always possible
  • Attribution: © OpenStreetMap contributors shown below map

Post detail overlay

Opening a post from the profile grid or notifications renders the full PostCard inside a centered Dialog (Dialog + DialogContent, showCloseButton={false}). The PostCard's own close button (X, top-right) and the 3-dots menu handle dismissal. The previous implementation used a bottom-anchored Sheet, which caused shadcn's auto-injected X to overlap the 3-dots button.

Welcome card (new users)

WelcomeCard appears at the top of FeedList when a pet has no posts yet and hasn't dismissed it. It lists 5 features (Posts, Stories, Milestones, DMs, Health) with icons. Dismissed state is stored in localStorage as pf_welcome_seen_{petId} and is per-pet. Both the X button and "Verstanden" button dismiss it.

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.

  • Orange — pet has unseen stories
  • Gray — pet has stories but viewer has seen them all
  • No ring — no active stories

The viewer identity comes from ActivePetContext inside PawRing itself — callers don't need to pass it.


Development Progress

Phase 1 — Foundation ✅

  • 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 ✅

  • 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 (orange/gray), 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 (opens full PostCard in Dialog with original-format image) + Milestones tab

Phase 3 — Engagement & Discovery ✅

  • Paw reactions: single reaction per post, toggle on/off, live count on PostCard
  • Comments: flat thread, create/delete own, CommentSheet drawer
  • Paw-Back (Repost): repost with attribution header in feed (PostType.REPOST)
  • Explore page: species tabs + breed filter, random fresh content
  • Search: pet name/breed search, owner username search (via Clerk), hashtag search with 3-col grid
  • Hashtags: extracted on post create, clickable #tags in captions navigate to hashtag feed

Phase 4 — Notifications ✅

  • Notification model with FOLLOW | REACTION | COMMENT | MESSAGE types
  • Fire-and-forget triggers in follows, reactions, and comments routers
  • NotificationBell in Sidebar + MobileNav (orange badge, 30s polling, "99+" cap)
  • /notifications page: list with actor avatar, type icon, relative time, post thumbnail
  • Clicking a notification navigates to the actor's profile (FOLLOW) or the relevant pet's profile (REACTION/COMMENT)
  • Auto-marks all read on page visit

Phase 5 — Video ✅

  • VideoPost model + VideoStatus enum (PROCESSING | READY | ERROR)
  • Mux direct upload: client PUTs directly to Mux, bytes never pass through Next.js server
  • VideoCard component polls getByPostId every 5s while PROCESSING; transitions to MuxPlayer when READY
  • getByPostId self-heals in dev by calling Mux API directly (no webhook tunnel required)
  • Mux webhook handler at /api/webhooks/mux for production (video.upload.asset_created, video.asset.ready, video.asset.errored)
  • Video posts appear in feed, profile grid, and notifications alongside photo posts

Phase 6 — Messaging ✅

  • Conversation + Message models; DmPolicy enum (EVERYONE | FOLLOWERS_ONLY) on Pet
  • Pet-to-pet DMs: inbox sorted by last message, conversation view with read/unread tracking
  • readAt timestamp per message; unread count badge in Sidebar + MobileNav
  • DM policy enforced server-side: FOLLOWERS_ONLY pets reject messages from non-followers
  • Conversation deduplication: petAId always < petBId (sorted on create) — one row per pair

Phase 7 — Health & Vet Tracking ✅

  • WeightLog, VetVisit, Vaccine, HealthCard, EmergencyVet models — all owner-private
  • Health dashboard at /pets/[petId]/health with three tabs: Gewicht / Tierarztbesuche / Impfungen
  • Weight trend chart: pure SVG bezier curve, no library, PawFeed orange-500 design, hover tooltips; shown when 2+ entries exist
  • WeightChart is a standalone reusable client component — used in both HealthDashboard and the public health card
  • Overdue vaccine badge (AlertTriangle + destructive variant) when nextDueDate is in the past
  • Emergency vet (private, owner dashboard only): Nominatim search → OSM map; tRPC procedures with assertOwner
  • Shareable health card at /health-card/[token]:
    • Expiry selectable per share: 7 / 14 / 30 / 60 / 90 days (default 14)
    • Expired links show a German-language expiry message with the exact date
    • Link can be regenerated (old token invalidated) at any time
    • Shows: vaccines, weight trend chart, vet visits — not emergency vet (private contact data)
    • PDF export: "Als PDF öffnen / drucken" opens the card with ?print=1 which auto-triggers window.print() — browser-native, no PDF library
    • print:hidden Tailwind variant hides nav chrome during print

v2 Features — Feed & Engagement ✅

  • Algorithmic feed toggle ("Für dich" / "Aktuell") in FeedList

    • feed.getFeed accepts mode: "chrono" | "algo" (default "chrono")
    • Algo scoring: reactions×3 + comments×5 + originalReposts×4 + recency bonus
    • Preference persisted in localStorage (pf_feed_mode)
  • Anniversary notifications via Vercel cron (/api/cron/anniversaries, daily 08:00 UTC)

    • BIRTHDAY + ADOPTION_DAY notification types added to NotificationType enum
    • Pet fields birthday and adoptedAt (@db.Date) added to schema + pet edit form
    • Raw SQL with EXTRACT(MONTH/DAY FROM ...) to match today's day/month across all years
    • 1-year minimum guard on both types — no notification in the first year
    • Bearer token auth (Authorization: Bearer CRON_SECRET) for the route
    • Notification page: Cake icon (pink-500) for birthdays, Home icon (emerald-500) for adoption days; link navigates to /pets/{id}/health
  • Welcome card for new users (WelcomeCard)

    • Appears at top of feed when pet has 0 posts and hasn't dismissed it
    • Lists 5 features with icons: Camera (Posts), Flame (Stories), Heart (Milestones), MessageCircle (DMs), Activity (Health)
    • Orange gradient background (from-orange-50/60 to-background)
    • Dismissed per-pet via localStorage (pf_welcome_seen_{petId})
    • X button + "Verstanden" button both dismiss
  • Post detail overlay fix

    • Changed from Sheet side="bottom" to centered Dialog with showCloseButton={false}
    • Prevents shadcn's auto-injected close button from overlapping PostCard's 3-dots menu
    • max-w-[560px] max-h-[90dvh] overflow-y-auto rounded-xl p-0

What's Next (continue here tomorrow)

These are the natural next features. None are started — all are clean slates.

High priority

Feature Notes
Explore page improvements Currently shows random content. Could show trending posts (by reaction count in last 24h), top hashtags, featured pets. Add "Trending" tab alongside species tabs.
Story video support Stories currently only support photos. Add short video stories (Mux or direct upload). StoryViewer already has a progress timer — just needs video playback instead of <img>.
Follower milestone auto-notifications MilestoneType has FOLLOWERS_500, FOLLOWERS_1K etc. but no trigger exists yet. Should fire a milestone post automatically when follower count crosses these thresholds. Hook into follows.follow.
Google / Apple OAuth Clerk supports it natively — just enable in Clerk Dashboard. No code change required. User deliberately deferred this.

Medium priority

Feature Notes
Pet bio / adoption story on profile Fields exist in schema (bio, adoptionStory) but are not displayed on the profile page. Add them to the profile header.
Story reactions Stories currently only track views. Add a quick emoji reaction (🐾❤️😂) that appears as a floating animation.
Saved posts Bookmarking posts to a private collection. New SavedPost table, saved indicator on PostCard, saved-posts tab on profile.
Comment replies Flat comments work. Add 1-level reply threading (replyToId FK on Comment).
Admin dashboard Basic moderation view: list of reports, pet account overview, manual suspension. Could be a /admin route behind a role flag on Owner.

Low priority / post-MVP

Feature Notes
i18n (multi-language) EN/DE/ES/IT/FR planned. Species and breed names need translation tables — don't duplicate rows per language. Use a locale + key structure.
React Native app Listed in CLAUDE.md as a future milestone. Web-first for v1.
Supabase Realtime Feed live updates and DM delivery without polling. Replace setInterval polling in NotificationBell and conversation view with Supabase channel subscriptions on the relevant tables.
Cloudflare R2 + Images Currently using Supabase Storage. R2 has zero egress fees — migration makes sense at scale.

Open bugs / cleanup

Issue Notes
Feed Redis cleanup on post delete When a post is deleted, its ID is NOT removed from Redis sorted sets. feed.getFeed silently skips missing IDs (Prisma returns nothing for them) — functionally correct but wastes Redis space. Add cleanup on posts.delete.
Story views cleanup StoryView records are never deleted. A weekly cron should purge rows where viewedAt < NOW() - 30 days.
People search with no username search.byOwner via Clerk falls back to first/last name when no Clerk username is set. Works but inconsistent UX.
Welcome card logic Currently checks "pet has 0 posts" but the posts count comes from the feed data, not a dedicated query. Refactor to a posts.countByPet call in the health router or a dedicated RSC check.

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.

npx vitest run          # full suite
npx vitest              # watch mode

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.
After a Prisma schema change, always restart the dev server The Next.js dev server caches the Prisma client in memory. A running server started before prisma generate will not see new models.
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 Intentional: view analytics are retained 30 days. A separate cleanup cron should purge old views.
Feed cleanup on post delete is best-effort When a post is deleted, its ID is not removed from Redis sorted sets. feed.getFeed silently skips IDs that Prisma doesn't return.
Mux webhook not required in local dev videos.getByPostId polls Mux directly when status is PROCESSING. The DB is updated on the next poll cycle. In production, the webhook updates the DB faster (within seconds of transcode completion).
People search requires Clerk username to be set search.byOwner queries Clerk's getUserList. Users who signed up without a username will match by first/last name instead.
CRON_SECRET must be set in Vercel env vars The anniversary cron route checks Authorization: Bearer CRON_SECRET. Without it, the route returns 401 and no notifications fire.
S
Description
No description provided
Readme
56 MiB
1 Stars 1 Watchers 0 Forks
Languages
TypeScript 98.6%
Shell 0.5%
CSS 0.4%
HTML 0.2%
JavaScript 0.2%
Other 0.1%