# 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 (15–85th) · 🟡 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 `) | | `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.