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>
This commit is contained in:
parent
4dcdc5a572
commit
e2a3e83638
2 changed files with 247 additions and 259 deletions
459
CLAUDE.md
459
CLAUDE.md
|
|
@ -12,10 +12,13 @@ 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
|
||||
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
|
||||
|
||||
|
|
@ -23,10 +26,12 @@ docker-compose -f docker-compose.dev.yml exec db psql -U postgres -d tia
|
|||
|
||||
- **Framework:** Next.js 16 with App Router (src/app/)
|
||||
- **Database:** PostgreSQL 16 with pgvector + Drizzle ORM
|
||||
- **Auth:** Database sessions with httpOnly cookies
|
||||
- **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
|
||||
|
||||
|
|
@ -34,289 +39,265 @@ docker-compose -f docker-compose.dev.yml exec db psql -U postgres -d tia
|
|||
src/
|
||||
├── app/ # Next.js App Router pages
|
||||
│ ├── api/ # API routes (auth, logs, ai, growth, etc.)
|
||||
│ ├── page.tsx # Home (Quick Log + AI card)
|
||||
│ ├── 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
|
||||
│ ├── settings/ # Settings with theme picker, invite members
|
||||
│ ├── profile/ # Parent user profile (avatar, name, email)
|
||||
│ ├── login/ # User login (email + password)
|
||||
│ ├── admin/ # Admin panel
|
||||
│ └── admin-login/ # Admin login (separate)
|
||||
│ ├── 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)
|
||||
drizzle/ # Database migrations
|
||||
docs/ # Design docs
|
||||
├── 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:** 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
|
||||
## Database Migrations
|
||||
|
||||
### Data Models
|
||||
### 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
|
||||
|
||||
- **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/head measurements over time
|
||||
- **Memories:** Photos with R2 storage
|
||||
- **Chat Sessions:** User conversations with AI (chat_sessions, chat_messages)
|
||||
### 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
|
||||
|
||||
### Growth Page Features
|
||||
### Hot-fix: apply migration without waiting for redeploy
|
||||
Use the **debug-migration endpoint** (same pattern as the Circles implementation):
|
||||
|
||||
The growth page tracks child's physical development with WHO standards.
|
||||
|
||||
**Components:**
|
||||
- `formatAge(birthDate, measurementDate?)` - Shows age as "1y 4mo" or "8mo"
|
||||
- Latest Reading card with velocity and percentile
|
||||
- WHO Standards collapsible card (shows icons ⚖️📏⭕ when collapsed)
|
||||
- Growth chart (collapsible inside WHO card)
|
||||
- History (collapsible with scroll for many entries)
|
||||
- Goals stored in localStorage (`tia_growth_goal_${childId}`)
|
||||
|
||||
**State:**
|
||||
- `showAdd` - Toggle add form (only shows if latest exists)
|
||||
- `showGoals` - Toggle goals card
|
||||
- `showWhoStandards` - Toggle WHO card expand/collapse
|
||||
- `showChart` - Toggle chart inside WHO card
|
||||
- `showHistory` - Toggle history expand/collapse
|
||||
|
||||
**Color-coded percentiles:**
|
||||
- 🟢 Normal (15th-85th percentile)
|
||||
- 🟡 Watch (<15th or >85th)
|
||||
- 🔴 Alert (<3rd or >97th)
|
||||
|
||||
**Column order:** Goals → Latest → Add Form → WHO+Chart → History
|
||||
|
||||
### 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"
|
||||
```
|
||||
POST /api/debug-migration
|
||||
Header: x-run-migration: yes
|
||||
```
|
||||
|
||||
**FamilyProvider:** Resolves family from database session on login.
|
||||
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 } = useFamily();
|
||||
// familyId: string | null
|
||||
// familyName: string | null (from session)
|
||||
// child: Child | null
|
||||
// children: Child[]
|
||||
// tier: "free" | "pro"
|
||||
// memberCount: number (from family_members table)
|
||||
const { familyId, familyName, child, children, tier, memberCount, updateChildImage } = useFamily();
|
||||
// updateChildImage(childId, imageUrl | null) — updates in-memory state after photo change
|
||||
```
|
||||
|
||||
**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`:
|
||||
## Photo Uploads (R2)
|
||||
|
||||
```typescript
|
||||
import { validateSession, requireFamily, requireOwnership } from "@/lib/auth";
|
||||
### 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.
|
||||
|
||||
// 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 });
|
||||
}
|
||||
### 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
|
||||
|
||||
const familyId = auth.session!.familyId!;
|
||||
// ... route logic
|
||||
}
|
||||
### Photo storage — two completely separate features
|
||||
|
||||
// Require ownership for specific resources
|
||||
const ownership = await requireOwnership(childId, "children", "Child");
|
||||
if (!ownership.success) {
|
||||
return NextResponse.json({ error: ownership.error }, { status: ownership.status });
|
||||
}
|
||||
```
|
||||
| | **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}/...` |
|
||||
|
||||
**Chat Sessions:** Stored in localStorage (`tia_chat_sessions`) - shared between home page AI card and /ai page. Database tables: `chat_sessions`, `chat_messages`.
|
||||
### 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.
|
||||
|
||||
**API Routes:** Return standard JSON `{ success: true, items: [...] }` format for lists.
|
||||
### 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.
|
||||
|
||||
**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`:
|
||||
## 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 });
|
||||
}
|
||||
// ... route logic
|
||||
if (!auth.success) return NextResponse.json({ error: auth.error }, { status: auth.status });
|
||||
const familyId = auth.session!.familyId!;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 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`:
|
||||
|
||||
### ThemeProvider
|
||||
```typescript
|
||||
import { detectMedicalIntent } from "@/lib/ai/medical-triggers";
|
||||
|
||||
const intent = detectMedicalIntent(query);
|
||||
if (intent.isMedical) {
|
||||
// Redirect to pediatrician
|
||||
}
|
||||
import { useTheme } from "./ThemeProvider";
|
||||
const { theme, toggle, setMode } = useTheme();
|
||||
// theme: "light" | "dark" | mode: "light" | "dark" | "system" | "time"
|
||||
```
|
||||
|
||||
## Known Issues
|
||||
### 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 <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).
|
||||
|
||||
Some files may cause "Unterminated regexp literal" errors when building with Turbopack. If build fails:
|
||||
### 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`.
|
||||
|
||||
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
|
||||
### 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.
|
||||
|
|
|
|||
|
|
@ -65,8 +65,12 @@ export async function POST(request: Request) {
|
|||
[auth.session!.familyId, email, role || "caregiver", token, expiresAt.toISOString()]
|
||||
);
|
||||
|
||||
// Fetch inviter name + family name for the email
|
||||
// Send invite email — capture result so caller can see if it worked
|
||||
let emailStatus: { sent: boolean; error?: string; noKey?: boolean } = { sent: false };
|
||||
try {
|
||||
if (!process.env.RESEND_API_KEY) {
|
||||
emailStatus = { sent: false, noKey: true };
|
||||
} else {
|
||||
const [meta] = await sql.unsafe(
|
||||
`SELECT u.name as inviter_name, f.name as family_name
|
||||
FROM family_members fm
|
||||
|
|
@ -83,12 +87,15 @@ export async function POST(request: Request) {
|
|||
token,
|
||||
role: role || "caregiver",
|
||||
});
|
||||
emailStatus = { sent: true };
|
||||
}
|
||||
} catch (emailErr) {
|
||||
console.error("[INVITE-EMAIL-ERROR]", emailErr);
|
||||
// non-fatal — invite was created, email just didn't send
|
||||
const msg = emailErr instanceof Error ? emailErr.message : String(emailErr);
|
||||
console.error("[INVITE-EMAIL-ERROR]", msg);
|
||||
emailStatus = { sent: false, error: msg };
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, invite, inviteUrl: `/invite/${token}` });
|
||||
return NextResponse.json({ success: true, invite, inviteUrl: `/invite/${token}`, emailStatus });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return NextResponse.json({ error: String(error) }, { status: 500 });
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue