tia/CLAUDE.md
Mannu e2a3e83638 docs+fix: overhaul CLAUDE.md + expose emailStatus in invite response
CLAUDE.md:
- Add RESEND_API_KEY, EMAIL_FROM, NEXT_PUBLIC_APP_URL to required env vars
- Document DB migration pattern (journal + hot-fix via debug-migration POST)
- Document R2 3-step proxy upload pattern (CORS note)
- Document users.image (NOT avatar_url) and two separate photo features
- Document admin auth server-component pattern
- Document family_invites fix, invite flow, cancel invite
- Full data storage table with all localStorage keys

Invite route:
- Return emailStatus in POST response so caller can see if Resend fired
  or why it failed (noKey / error message)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 15:15:53 +05:30

303 lines
12 KiB
Markdown
Raw 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.

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Commands
```bash
# Development
pnpm dev # Start dev server at http://localhost:3000
pnpm build # Production build (uses Turbopack by default)
pnpm start # Start production server
# Note: If Turbopack fails, use: pnpm build -- --webpack
# Database (direct SQL or docker)
docker-compose -f docker-compose.dev.yml exec db psql -U postgres -d tia_dev
```
**Note:** Running from `/Users/manohar_air/MyProjects/Tia/tia` directory.
**Rule:** ALWAYS run `pnpm build` locally BEFORE git commit/push.
---
## Architecture
### Tech Stack
- **Framework:** Next.js 16 with App Router (src/app/)
- **Database:** PostgreSQL 16 with pgvector + Drizzle ORM
- **Auth:** Database sessions with httpOnly cookies (`tia_session`)
- **AI:** LiteLLM gateway → MiniMax model (minimax-2.7)
- **Storage:** Cloudflare R2 for media uploads
- **Email:** Resend (transactional emails)
- **Styling:** Tailwind CSS v4
- **Deploy:** Dokploy (Docker-based, auto-runs migrations on deploy)
### Project Structure
```
src/
├── app/ # Next.js App Router pages
│ ├── api/ # API routes (auth, logs, ai, growth, etc.)
│ ├── page.tsx # Home (Quick Log + AI card + baby card)
│ ├── ai/ # AI chat page with sidebar
│ ├── medical/ # Vaccination tracking
│ ├── growth/ # Growth charts
│ ├── memories/ # Photo gallery
│ ├── menu/ # Navigation menu
│ ├── onboarding/ # First-time setup
│ ├── settings/ # Settings with theme picker, invite members
│ ├── profile/ # Parent user profile (avatar, name, email)
│ ├── login/ # User login (email + password)
│ ├── admin/ # Admin panel (server component layout)
│ └── admin-login/ # Admin login (separate, NOT under admin layout)
├── ThemeProvider.tsx # Theme context (light/dark/system/time)
├── FamilyProvider.tsx # Family/child context (resolves from session)
├── lib/
│ ├── auth.ts # requireFamily(), requireOwnership()
│ ├── email.ts # sendVerificationEmail(), sendFamilyInviteEmail()
│ └── admin-auth.ts # requireAdmin() for API, verifyAdminSession() for server components
drizzle/ # Database migrations (SQL files)
drizzle/meta/_journal.json # Migration order — MUST update when adding new SQL files
```
---
## Database Migrations
### How it works (Dokploy auto-deploy)
- `Dockerfile CMD`: `node migrate.mjs && node server.js` — migrations run first, app boots after
- `src/db/migrate.ts`: reads every SQL file in `drizzle/` and applies any not yet in `__drizzle_migrations`
- `drizzle/meta/_journal.json`: ordered list of migration files — **must be updated** when adding a new SQL file
### Adding a new migration
1. Create `drizzle/NNNN_description.sql` with your SQL (use `IF NOT EXISTS` / `ADD COLUMN IF NOT EXISTS`)
2. Add an entry to `drizzle/meta/_journal.json` with the next `idx` and matching `tag`
3. Push to git → Dokploy auto-applies on next deploy
### Hot-fix: apply migration without waiting for redeploy
Use the **debug-migration endpoint** (same pattern as the Circles implementation):
```
POST /api/debug-migration
Header: x-run-migration: yes
```
This runs SQL directly through the app's live DB connection. Steps use `IF NOT EXISTS` so safe to re-run.
To add a new hot-fix: add `ALTER TABLE ... ADD COLUMN IF NOT EXISTS` to the `steps` array in
`src/app/api/debug-migration/route.ts`, push, and immediately call the endpoint from Chrome:
```javascript
// In browser console on tia.manohargupta.com
const res = await fetch('/api/debug-migration', {
method: 'POST',
headers: { 'x-run-migration': 'yes' }
});
console.log(await res.json());
```
The GET endpoint also shows which migrations have been applied and email config status.
---
## Authentication
### User auth (email + password)
1. User visits `/login` with email + password
2. `POST /api/auth/signin` verifies bcrypt hash, creates session in `sessions` table
3. Session token stored in **httpOnly cookie** `tia_session` (NOT localStorage!)
4. All data routes use `requireFamily()` from `@/lib/auth`
### Admin auth
- Login at `/admin-login` (username: `admin`, password: from env)
- Sets `tia_admin_session` httpOnly cookie
- **Admin layout** (`src/app/admin/layout.tsx`) is a **server component** — calls `verifyAdminSession()` from `@/lib/admin-auth` and `redirect('/admin-login')` if not authed
- **Admin sub-pages** (`families`, `users`, `analytics`, etc.) use `credentials: 'include'` on all fetches — NO localStorage tokens, NO Bearer headers
- `AdminSidebar.tsx` is a separate `"use client"` component for the interactive sidebar
### FamilyProvider
Resolves family from database session on login. Skip for admin routes.
```typescript
import { useFamily } from "./FamilyProvider";
const { familyId, familyName, child, children, tier, memberCount, updateChildImage } = useFamily();
// updateChildImage(childId, imageUrl | null) — updates in-memory state after photo change
```
---
## Photo Uploads (R2)
### Critical: CORS — never do a direct cross-origin PUT from the browser
Direct `PUT` to R2 presigned URLs is cross-origin and **blocked by the browser**. Always proxy through the server.
### 3-step upload pattern (used everywhere)
1. `POST /api/[init-endpoint]` with `{ contentType, filename }` → get `{ key, publicUrl }`
2. `PUT /api/upload?key=...&contentType=...` — server proxies the file to R2
3. `PATCH /api/[save-endpoint]` with `{ [imageField]: publicUrl }` → saves URL to DB + deletes old R2 object
### Photo storage — two completely separate features
| | **Parent profile photo** | **Baby card photo** |
|---|---|---|
| Page | `/profile` | Homepage (`/`) |
| DB column | `users.image` | `children.image_url` |
| API | `POST/PATCH/DELETE /api/auth/avatar` | `POST/PATCH /api/children/[id]` |
| R2 prefix | `avatars/{userId}/...` | `profiles/{childId}/...` |
### Orphan cleanup pattern
Before overwriting an image URL in the DB, fetch the old URL, extract the R2 key, verify it's under a controlled prefix (`avatars/` or `profiles/`), then `DeleteObjectCommand`. Applied in both avatar and children routes.
### users.image — NOT users.avatar_url
The `users` table column is called **`image`** (not `avatar_url`). `avatar_url` belongs to `member_profiles`.
Always use `u.image` in SQL queries against the `users` table.
---
## Key Patterns
### Session validation in API routes
```typescript
import { requireFamily, requireOwnership } from "@/lib/auth";
export async function GET(request: Request) {
const auth = await requireFamily();
if (!auth.success) return NextResponse.json({ error: auth.error }, { status: auth.status });
const familyId = auth.session!.familyId!;
// ...
}
```
### ThemeProvider
```typescript
import { useTheme } from "./ThemeProvider";
const { theme, toggle, setMode } = useTheme();
// theme: "light" | "dark" | mode: "light" | "dark" | "system" | "time"
```
### Offline Queue
Uses localStorage (`tia_offline_queue`) for failed API calls, retries when online.
### Chat Sessions
Stored in localStorage (`tia_chat_sessions`) — shared between home page AI card and `/ai` page.
Database tables: `chat_sessions`, `chat_messages`.
---
## Email (Resend)
Transactional emails via Resend. Functions in `src/lib/email.ts`:
- `sendVerificationEmail(email, token, userId)` — account verification
- `sendFamilyInviteEmail({ to, inviterName, familyName, token, role })` — family invite
Emails fall back to console log if `RESEND_API_KEY` is not set (dev mode).
---
## Family Invites
Flow:
1. Settings → Invite → `POST /api/invites` → creates row in `family_invites`, sends email via Resend
2. Invitee clicks `/invite/{token}` link → logs in/signs up
3. `POST /api/invites/accept` → adds to `family_members`, **deletes** the invite row (single-use)
4. `DELETE /api/invites/[id]` — cancel a pending invite
`family_invites` table columns: `id, family_id, email, role, token, expires_at, created_at, display_name, accepted_at`
(Note: `display_name` and `accepted_at` were missing from the original schema — added in migration `0006`)
---
## Growth Page
- `formatAge(birthDate, measurementDate?)` — shows age as "1y 4mo" or "8mo"
- Goals stored in localStorage (`tia_growth_goal_${childId}`)
- Column order: Goals → Latest Reading → Add Form → WHO+Chart → History
- Color-coded percentiles: 🟢 Normal (1585th) · 🟡 Watch (<15 or >85) · 🔴 Alert (<3 or >97)
---
## Admin Panel
Access at `/admin-login`.
Pages: `/admin`, `/admin/families`, `/admin/users`, `/admin/children`, `/admin/revenue`, `/admin/analytics`, `/admin/support`, `/admin/settings`
Auth pattern: server component layout calls `verifyAdminSession()` → redirects to `/admin-login` if not authenticated. No client-side cookie checks.
---
## Data Storage Rules
| Data Type | Storage | API |
|----------|---------|-----|
| Children | Database | `/api/children` |
| Activity Logs | Database | `/api/logs` |
| Vaccinations | Database | `/api/vaccinations` |
| Growth Records | Database | `/api/growth` |
| User Profile | Database | `/api/auth/profile` |
| Parent Avatar | Database (`users.image`) + R2 | `/api/auth/avatar` |
| Baby Photo | Database (`children.image_url`) + R2 | `/api/children/[id]` |
| Memories/Photos | Database + R2 | `/api/upload` |
| Auth Session | Database + Cookie | `/api/auth/signin` |
| Theme | localStorage | `tia_theme` |
| Growth Goals | localStorage | `tia_growth_goal_${childId}` |
| Custom Folders | localStorage | `tia_custom_folders` |
| Offline Queue | localStorage | `tia_offline_queue` |
**NEVER use localStorage for:** authentication tokens, family_id, or any data that should persist across devices.
---
## Environment Variables
Set in `.env.local` for development, Dokploy dashboard for production.
| Variable | Required | Description |
|----------|----------|-------------|
| `DATABASE_URL` | ✅ | PostgreSQL connection |
| `DATABASE_URL_SUPERUSER` | ✅ | Superuser connection (migrations only) |
| `LITELLM_BASE_URL` | ✅ | AI gateway URL |
| `LITELLM_API_KEY` | ✅ | AI API key |
| `R2_ACCOUNT_ID` | ✅ | Cloudflare R2 account ID |
| `R2_ACCESS_KEY_ID` | ✅ | R2 access key |
| `R2_SECRET_ACCESS_KEY` | ✅ | R2 secret key |
| `R2_BUCKET_NAME` | ✅ | R2 bucket name (e.g. "tia") |
| `R2_PUBLIC_URL` | ✅ | Public R2 URL |
| `RESEND_API_KEY` | ✅ | Resend API key for transactional email |
| `EMAIL_FROM` | ✅ | Sender address (e.g. `Tia <tia@manohargupta.com>`) |
| `NEXT_PUBLIC_APP_URL` | ✅ | Full app URL (e.g. `https://tia.manohargupta.com`) |
| `CRON_SECRET` | ✅ | Secret for cron backup endpoint |
---
## R2 Storage (Cloudflare)
- Account ID: `e71f22a2f8614fb3ba6d9b28a264d8ce`
- S3 Endpoint: `https://e71f22a2f8614fb3ba6d9b28a264d8ce.r2.cloudflarestorage.com`
- Public URL: `https://pub-37a76fd657c94d1dbc521a109c087a11.r2.dev` (no bucket name in path!)
Key learnings:
1. S3 API endpoint does NOT include bucket name
2. Public URL does NOT include `/tia/` bucket path
3. Direct browser PUT to presigned URL = CORS blocked → always proxy via `/api/upload`
---
## Known Issues & Fixes Applied
### Turbopack Parsing Issue
Some files cause "Unterminated regexp literal" with Turbopack. Fix: `pnpm build -- --webpack`
Avoid patterns like `name: "TT/Td"` in arrays (the `/` can be misread as regex).
### users.image vs users.avatar_url
The `users` table uses `image` for the profile photo column. Using `avatar_url` in SQL will throw
"column does not exist". `avatar_url` exists only on `member_profiles`.
### family_invites missing columns
Original schema was missing `display_name` and `accepted_at`. Added in `drizzle/0006_family_invites_missing_cols.sql`.
Hot-fixed via `/api/debug-migration` POST.
### Admin auth (httpOnly cookie)
`tia_admin_session` is httpOnly — `document.cookie` can never read it. Admin pages use a **server component layout**
that calls `verifyAdminSession()` server-side. Client pages use `credentials: 'include'` on fetches; no Bearer tokens.