Growth table had RLS blocking writes. API has requireOwnership checks, so disabling RLS is secure. Changed table owner to tia_app. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
296 lines
10 KiB
Markdown
296 lines
10 KiB
Markdown
# 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
|
|
```
|
|
|
|
**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.
|
|
|
|
```typescript
|
|
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.
|
|
|
|
```typescript
|
|
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`:
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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`:
|
|
|
|
```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 });
|
|
}
|
|
// ... route logic
|
|
}
|
|
```
|
|
|
|
### Current Security Status (May 2026)
|
|
|
|
- **RLS (Row-Level Security):** DISABLED on family_members, children, and growth tables (app-level security via requireOwnership)
|
|
- **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`:
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```bash
|
|
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
|
|
|