tia/CLAUDE.md
Mannu 318b277e44 feat(growth): add edit/delete, goals, CSV export, and WHO percentile enhancements
- Add PUT and DELETE endpoints for growth records
- Add CSV export for pediatrician visits
- Add goal tracking with localStorage persistence
- Color-coded percentiles (green/yellow/red zones)
- Show WHO percentile lines (15th, 50th, 85th) on chart
- Growth velocity indicator (kg/month between readings)
- Enhanced WHO standards card with actual vs target + goal progress
- Better empty state with encouraging prompt
- Fix UUID type for growth record IDs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 13:52:02 +05:30

10 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Commands

# 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

Note: Running from /Users/manohar_air/MyProjects/Tia/tia directory.

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
  • AI: LiteLLM gateway → MiniMax model (minimax-2.7)
  • Storage: Cloudflare R2 for media uploads
  • Styling: Tailwind CSS v4

Project Structure

src/
├── app/                    # Next.js App Router pages
│   ├── api/               # API routes (auth, logs, ai, growth, etc.)
│   ├── page.tsx            # Home (Quick Log + AI 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
│   ├── login/            # User login (email + password)
│   ├── admin/            # Admin panel
│   └── admin-login/     # Admin login (separate)
├── ThemeProvider.tsx       # Theme context (light/dark/system/time)
├── FamilyProvider.tsx      # Family/child context (resolves from session)
drizzle/                  # Database migrations
docs/                     # Design docs

Database

  • Migrations: SQL files in drizzle/ (not using drizzle-kit push)
  • Apply: psql directly or via docker-compose exec
  • RLS: Row-level security for multi-family isolation

Data Models

  • Family: Parent account container
  • Members: Adults in family (mom, dad, etc.) via family_members
  • Children: Baby profiles with birth date
  • Sessions: Login sessions with httpOnly cookies
  • Logs: Feed, sleep, diaper entries with timestamps
  • Vaccinations: IAP schedule tracking
  • Growth: Weight/height over time
  • Memories: Photos with R2 storage
  • Chat Sessions: User conversations with AI (chat_sessions, chat_messages)

Key Patterns

ThemeProvider: Wrap app in ThemeProvider from layout.tsx. Use useTheme() hook in components.

import { useTheme } from "./ThemeProvider";
const { theme, toggle, setMode } = useTheme();
// theme: "light" | "dark"
// mode: "light" | "dark" | "system" | "time"

FamilyProvider: Resolves family from database session on login.

import { useFamily } from "./FamilyProvider";
const { familyId, familyName, child, children, tier, memberCount } = useFamily();
// familyId: string | null
// familyName: string | null (from session)
// child: Child | null
// children: Child[]
// tier: "free" | "pro"
// memberCount: number (from family_members table)

Offline Queue: Uses localStorage (tia_offline_queue) for failed API calls, retries when online.

Session Validation: All data API routes must use functions from @/lib/auth:

import { validateSession, requireFamily, requireOwnership } from "@/lib/auth";

// Require family for user data
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!;
  // ... route logic
}

// Require ownership for specific resources
const ownership = await requireOwnership(childId, "children", "Child");
if (!ownership.success) {
  return NextResponse.json({ error: ownership.error }, { status: ownership.status });
}

Chat Sessions: Stored in localStorage (tia_chat_sessions) - shared between home page AI card and /ai page. Database tables: chat_sessions, chat_messages.

API Routes: Return standard JSON { success: true, items: [...] } format for lists.

AI Integration:

  • Route: /api/ai → LiteLLM (set via LITELLM_BASE_URL env var)
  • Model: minimax-2.7
  • See /docs/debugging.md for troubleshooting

Authentication (Email + Password)

  1. User visits /login with email + password (or signs up for new account)
  2. API /api/auth/signin verifies password hash and creates session in sessions table
  3. Session token stored in httpOnly cookie (NOT localStorage!)
  4. Password stored with simple hash in users.password_hash

Tables Used

  • users: User accounts (email, name)
  • families: Family accounts (name, tier, limits)
  • family_members: Links users to families (user_id, family_id, role)
  • children: Child profiles (name, birth_date, family_id)
  • sessions: Login sessions (session_token, user_id, expires)

NEVER use localStorage for:

  • authentication tokens
  • family_id after login
  • Any data that should persist across devices

localStorage Acceptable For:

  • Theme preference (user-specific display only)
  • Temporary cache (offline queue for retry)
  • Chat sessions local cache (synced from database)

Admin Panel

Access at: /admin-login (username: admin, password: admin123)

Pages

  • /admin - Dashboard with clickable stat cards
  • /admin/families - Manage families (create, view/add/remove members, set tier)
  • /admin/users - Manage users (add to family, password status, delete)
  • /admin/children - Manage children
  • /admin/revenue - Revenue analytics
  • /admin/analytics - Feature usage
  • /admin/support - Support tickets
  • /admin/settings - Platform settings

Data Storage Consistency

RULE: All user data must persist to database, NOT localStorage

Data Type Storage API Key Persists After Refresh Persists After Logout
Children Database /api/children Yes Yes
Activity Logs Database /api/logs Yes Yes
Vaccinations Database /api/vaccinations Yes Yes
Growth Records Database /api/growth Yes Yes
User Profile Database /api/auth/profile Yes Yes
Memories/Photos Database + R2 /api/upload Yes Yes
Auth Session Database + Cookie /api/auth/signin Yes No
Theme localStorage tia_theme Yes Yes
Chat Sessions Database /api/chat Yes Yes
Offline Queue localStorage tia_offline_queue Yes No

R2 Storage (Cloudflare)

Setup

  1. Create bucket in Cloudflare Dashboard → R2
  2. Create API token with "Object Read & Write" permissions
  3. Enable Public Development URL in bucket settings (gives pub-*.r2.dev URL)

API Endpoint Format

The S3 API endpoint is: https://<accountId>.r2.cloudflarestorage.com

For your bucket named "tia":

  • Account ID: e71f22a2f8614fb3ba6d9b28a264d8ce
  • S3 Endpoint: https://e71f22a2f8614fb3ba6d9b28a264d8ce.r2.cloudflarestorage.com
  • Public URL: https://pub-37a76fd657c94d1dbc521a109c087a11.r2.dev (no bucket name in path!)

Code Example

const client = new S3Client({
  region: "auto",
  endpoint: `https://${accountId}.r2.cloudflarestorage.com`,
  credentials: { accessKeyId, secretAccessKey },
});

// List objects
const command = new ListObjectsV2Command({ Bucket: "tia" });
const res = await client.send(command);

// Get presigned upload URL
const command = new PutObjectCommand({ Bucket: "tia", Key: key, ContentType });
const url = await getSignedUrl(client, command, { expiresIn: 3600 });

Key Learnings

  1. S3 API endpoint does NOT include bucket name: https://...r2.cloudflarestorage.com
  2. Public URL does NOT include bucket path: https://pub-...r2.dev (NOT /tia/...)
  3. CORS needs GET and PUT methods for uploads
  4. ListObjects needs bucket name in command, not endpoint

Environment Variables

Set in .env.local for development, or in Dokploy dashboard for production.

Required:

  • DATABASE_URL - PostgreSQL connection (as tia_app role after H2.1)
  • DATABASE_URL_SUPERUSER - Superuser connection (for migrations only)
  • LITELLM_BASE_URL - AI gateway URL (e.g., https://llm.manohargupta.com)
  • 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
  • CRON_SECRET - Secret for cron backup endpoint

Security Patterns

All data API routes must validate sessions using @/lib/auth:

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 });
  }
  // ... route logic
}

Current Security Status (May 2026)

  • RLS (Row-Level Security): DISABLED on family_members and children tables (was blocking INSERTs)
  • App-level security: All routes use requireFamily() and requireOwnership() checks
  • This is secure because: All API routes validate session before returning data
  • To re-enable RLS later: Add proper INSERT bypass policy, keep RLS for SELECT only

AI routes use medical guardrails from @/lib/ai/medical-triggers:

import { detectMedicalIntent } from "@/lib/ai/medical-triggers";

const intent = detectMedicalIntent(query);
if (intent.isMedical) {
  // Redirect to pediatrician
}

Known Issues

Turbopack Parsing Issue

Some files may cause "Unterminated regexp literal" errors when building with Turbopack. If build fails:

  1. Try building with Webpack instead:

    pnpm build -- --webpack
    
  2. The issue is in SWC parser - avoid patterns like name: "TT/Td" in arrays as the / can be interpreted as regex

  3. Apply fixes one change at a time between builds - cumulative changes can confuse Turbopack's cache