docs: update README with v2 features, full schema overview, and next-session roadmap
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
23c570b2af
commit
38952b1563
1 file changed
+170
-21
@@ -30,7 +30,7 @@ A social platform where pets are the stars. Dogs, cats, and birds get their own
|
||||
- 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 owner-private.** The only publicly accessible health surface is the shareable health card (token URL with expiry), designed for vets, friends, and pet sitters.
|
||||
- **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.
|
||||
|
||||
---
|
||||
|
||||
@@ -46,7 +46,7 @@ src/
|
||||
pets/[petId]/edit/
|
||||
pets/[petId]/followers/
|
||||
pets/[petId]/following/
|
||||
pets/[petId]/health/ # Owner-only health dashboard (weight, vet visits, vaccines)
|
||||
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
|
||||
@@ -55,18 +55,22 @@ src/
|
||||
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)
|
||||
profile/ # ProfileTabs (Posts grid + Milestones list) — post detail as centered Dialog
|
||||
safety/ # BlockDialog, ReportSheet
|
||||
social/ # FollowButton
|
||||
stories/ # PawRing, StoryViewer, StoryTray, StoryForm
|
||||
@@ -93,11 +97,11 @@ src/
|
||||
query-client.ts # TanStack Query client factory
|
||||
routers/
|
||||
_app.ts # Root router
|
||||
pets.ts # CRUD for pet profiles + getProfile (no ownership check)
|
||||
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 via Redis + Prisma hydration)
|
||||
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)
|
||||
@@ -110,7 +114,7 @@ src/
|
||||
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, shareable health card
|
||||
health.ts # Weight logs, vet visits, vaccines, emergency vet, shareable health card
|
||||
__tests__/
|
||||
helpers/
|
||||
prisma-mock.ts # Typed mock PrismaClient factory
|
||||
@@ -124,18 +128,49 @@ src/
|
||||
milestones.test.ts
|
||||
middleware.ts # Next.js middleware (Clerk auth guard)
|
||||
prisma/
|
||||
schema.prisma # Full DB schema (Owner, Pet, Post, Story, Follow, Block, Report,
|
||||
# Reaction, Comment, Repost, Hashtag, Notification, VideoPost,
|
||||
# Conversation, Message, WeightLog, VetVisit, Vaccine, HealthCard)
|
||||
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
|
||||
@@ -175,8 +210,9 @@ cp .env.example .env.local
|
||||
| `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. They are imported exclusively in `server-only` modules and never exposed to the client.
|
||||
> `SUPABASE_SERVICE_ROLE_KEY` and `MUX_TOKEN_SECRET` are server-only secrets — never exposed to the client.
|
||||
|
||||
### 3. Database setup
|
||||
|
||||
@@ -215,7 +251,11 @@ In Mux Dashboard -> Settings -> Webhooks, add an endpoint pointing to `https://y
|
||||
|
||||
> 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. Run the dev server
|
||||
### 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
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
@@ -282,25 +322,62 @@ Post created
|
||||
└── fanOutPost(redis, postId, petId, createdAt, prisma)
|
||||
└── for each follower: ZADD feed:{followerPetId} score=timestamp member=postId
|
||||
|
||||
feed.getFeed({ petId, cursor })
|
||||
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 triggers are wrapped in `.catch(() => {})` — they never crash the parent mutation.
|
||||
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) is owner-private. The only publicly accessible health surface is the **HealthCard**: a token URL with a user-selected expiry (7 / 14 / 30 / 60 / 90 days). The card page checks expiry server-side before rendering. Opening the card with `?print=1` auto-triggers `window.print()` for browser-native PDF export — no PDF library required.
|
||||
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)
|
||||
|
||||
@@ -340,7 +417,7 @@ The viewer identity comes from `ActivePetContext` inside `PawRing` itself — ca
|
||||
- **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 sheet with original-format image) + Milestones tab
|
||||
- **Profile:** Posts 3-col grid (opens full PostCard in Dialog with original-format image) + Milestones tab
|
||||
|
||||
### Phase 3 — Engagement & Discovery ✅
|
||||
|
||||
@@ -379,19 +456,90 @@ The viewer identity comes from `ActivePetContext` inside `PawRing` itself — ca
|
||||
|
||||
### Phase 7 — Health & Vet Tracking ✅
|
||||
|
||||
- `WeightLog`, `VetVisit`, `Vaccine`, `HealthCard` models — all owner-private
|
||||
- Health dashboard at `/pets/[petId]/health` with three tabs: Weight / Tierarztbesuche / Impfungen
|
||||
- `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 an extracted, reusable client component (`src/components/health/WeightChart.tsx`)
|
||||
- `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
|
||||
- **Shareable health card** at `/health-card/[token]` — no login required
|
||||
- **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
|
||||
- Weight trend chart included in the card view
|
||||
- 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
|
||||
@@ -414,7 +562,8 @@ npx vitest # watch mode
|
||||
| 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 | This is intentional (D-08): view analytics are retained 30 days. A separate cleanup job will purge old views. |
|
||||
| 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. |
|
||||
Reference in new issue
Block a user