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:
Manohar Gupta 2026-05-24 15:15:53 +05:30
parent 4dcdc5a572
commit e2a3e83638
2 changed files with 247 additions and 259 deletions

459
CLAUDE.md
View file

@ -12,10 +12,13 @@ pnpm start # Start production server
# Note: If Turbopack fails, use: pnpm build -- --webpack # Note: If Turbopack fails, use: pnpm build -- --webpack
# Database (direct SQL or docker) # 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. **Note:** Running from `/Users/manohar_air/MyProjects/Tia/tia` directory.
**Rule:** ALWAYS run `pnpm build` locally BEFORE git commit/push.
---
## Architecture ## 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/) - **Framework:** Next.js 16 with App Router (src/app/)
- **Database:** PostgreSQL 16 with pgvector + Drizzle ORM - **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) - **AI:** LiteLLM gateway → MiniMax model (minimax-2.7)
- **Storage:** Cloudflare R2 for media uploads - **Storage:** Cloudflare R2 for media uploads
- **Email:** Resend (transactional emails)
- **Styling:** Tailwind CSS v4 - **Styling:** Tailwind CSS v4
- **Deploy:** Dokploy (Docker-based, auto-runs migrations on deploy)
### Project Structure ### Project Structure
@ -34,289 +39,265 @@ docker-compose -f docker-compose.dev.yml exec db psql -U postgres -d tia
src/ src/
├── app/ # Next.js App Router pages ├── app/ # Next.js App Router pages
│ ├── api/ # API routes (auth, logs, ai, growth, etc.) │ ├── 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 │ ├── ai/ # AI chat page with sidebar
│ ├── medical/ # Vaccination tracking │ ├── medical/ # Vaccination tracking
│ ├── growth/ # Growth charts │ ├── growth/ # Growth charts
│ ├── memories/ # Photo gallery │ ├── memories/ # Photo gallery
│ ├── menu/ # Navigation menu │ ├── menu/ # Navigation menu
│ ├── onboarding/ # First-time setup │ ├── 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) │ ├── login/ # User login (email + password)
│ ├── admin/ # Admin panel │ ├── admin/ # Admin panel (server component layout)
│ └── admin-login/ # Admin login (separate) │ └── admin-login/ # Admin login (separate, NOT under admin layout)
├── ThemeProvider.tsx # Theme context (light/dark/system/time) ├── ThemeProvider.tsx # Theme context (light/dark/system/time)
├── FamilyProvider.tsx # Family/child context (resolves from session) ├── FamilyProvider.tsx # Family/child context (resolves from session)
drizzle/ # Database migrations ├── lib/
docs/ # Design docs │ ├── 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) ## Database Migrations
- **Apply:** `psql` directly or via docker-compose exec
- **RLS:** Row-level security for multi-family isolation
### 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 ### Adding a new migration
- **Members:** Adults in family (mom, dad, etc.) via `family_members` 1. Create `drizzle/NNNN_description.sql` with your SQL (use `IF NOT EXISTS` / `ADD COLUMN IF NOT EXISTS`)
- **Children:** Baby profiles with birth date 2. Add an entry to `drizzle/meta/_journal.json` with the next `idx` and matching `tag`
- **Sessions:** Login sessions with httpOnly cookies 3. Push to git → Dokploy auto-applies on next deploy
- **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)
### 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. ```
POST /api/debug-migration
**Components:** Header: x-run-migration: yes
- `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"
``` ```
**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 ```typescript
import { useFamily } from "./FamilyProvider"; import { useFamily } from "./FamilyProvider";
const { familyId, familyName, child, children, tier, memberCount } = useFamily(); const { familyId, familyName, child, children, tier, memberCount, updateChildImage } = useFamily();
// familyId: string | null // updateChildImage(childId, imageUrl | null) — updates in-memory state after photo change
// 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`: ## Photo Uploads (R2)
```typescript ### Critical: CORS — never do a direct cross-origin PUT from the browser
import { validateSession, requireFamily, requireOwnership } from "@/lib/auth"; Direct `PUT` to R2 presigned URLs is cross-origin and **blocked by the browser**. Always proxy through the server.
// Require family for user data ### 3-step upload pattern (used everywhere)
export async function GET(request: Request) { 1. `POST /api/[init-endpoint]` with `{ contentType, filename }` → get `{ key, publicUrl }`
const auth = await requireFamily(); 2. `PUT /api/upload?key=...&contentType=...` — server proxies the file to R2
if (!auth.success) { 3. `PATCH /api/[save-endpoint]` with `{ [imageField]: publicUrl }` → saves URL to DB + deletes old R2 object
return NextResponse.json({ error: auth.error }, { status: auth.status });
}
const familyId = auth.session!.familyId!; ### Photo storage — two completely separate features
// ... route logic
}
// Require ownership for specific resources | | **Parent profile photo** | **Baby card photo** |
const ownership = await requireOwnership(childId, "children", "Child"); |---|---|---|
if (!ownership.success) { | Page | `/profile` | Homepage (`/`) |
return NextResponse.json({ error: ownership.error }, { status: ownership.status }); | 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) ## Key Patterns
- 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`:
### Session validation in API routes
```typescript ```typescript
import { requireFamily, requireOwnership } from "@/lib/auth"; import { requireFamily, requireOwnership } from "@/lib/auth";
export async function GET(request: Request) { export async function GET(request: Request) {
const auth = await requireFamily(); const auth = await requireFamily();
if (!auth.success) { if (!auth.success) return NextResponse.json({ error: auth.error }, { status: auth.status });
return NextResponse.json({ error: auth.error }, { status: auth.status }); const familyId = auth.session!.familyId!;
} // ...
// ... route logic
} }
``` ```
### Current Security Status (May 2026) ### ThemeProvider
- **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 ```typescript
import { detectMedicalIntent } from "@/lib/ai/medical-triggers"; import { useTheme } from "./ThemeProvider";
const { theme, toggle, setMode } = useTheme();
const intent = detectMedicalIntent(query); // theme: "light" | "dark" | mode: "light" | "dark" | "system" | "time"
if (intent.isMedical) {
// Redirect to pediatrician
}
``` ```
## 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 (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 ### 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: ### family_invites missing columns
Original schema was missing `display_name` and `accepted_at`. Added in `drizzle/0006_family_invites_missing_cols.sql`.
```bash Hot-fixed via `/api/debug-migration` POST.
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
### 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.

View file

@ -65,30 +65,37 @@ export async function POST(request: Request) {
[auth.session!.familyId, email, role || "caregiver", token, expiresAt.toISOString()] [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 { try {
const [meta] = await sql.unsafe( if (!process.env.RESEND_API_KEY) {
`SELECT u.name as inviter_name, f.name as family_name emailStatus = { sent: false, noKey: true };
FROM family_members fm } else {
JOIN users u ON u.id = fm.user_id const [meta] = await sql.unsafe(
JOIN families f ON f.id = fm.family_id `SELECT u.name as inviter_name, f.name as family_name
WHERE fm.family_id = $1 AND fm.user_id = $2 FROM family_members fm
LIMIT 1`, JOIN users u ON u.id = fm.user_id
[auth.session!.familyId, auth.session!.userId] JOIN families f ON f.id = fm.family_id
); WHERE fm.family_id = $1 AND fm.user_id = $2
await sendFamilyInviteEmail({ LIMIT 1`,
to: email, [auth.session!.familyId, auth.session!.userId]
inviterName: meta?.inviter_name || "Someone", );
familyName: meta?.family_name || "your family", await sendFamilyInviteEmail({
token, to: email,
role: role || "caregiver", inviterName: meta?.inviter_name || "Someone",
}); familyName: meta?.family_name || "your family",
token,
role: role || "caregiver",
});
emailStatus = { sent: true };
}
} catch (emailErr) { } catch (emailErr) {
console.error("[INVITE-EMAIL-ERROR]", emailErr); const msg = emailErr instanceof Error ? emailErr.message : String(emailErr);
// non-fatal — invite was created, email just didn't send 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) { } catch (error) {
console.error(error); console.error(error);
return NextResponse.json({ error: String(error) }, { status: 500 }); return NextResponse.json({ error: String(error) }, { status: 500 });