API Routes
All API routes live under app/api/ and follow the Next.js App Router convention. All responses use the envelope { data, error }.
Habits
GET /api/habits
Returns all habits for the authenticated user's active season.
POST /api/habits
Creates a new habit. Body: { title, category, targetPerWeek }.
PATCH /api/habits/[id]
Updates a habit title, category, or target.
DELETE /api/habits/[id]
Deletes a habit and all its logs.
Logs
POST /api/logs
Records a daily check-in. Body: { habitId, logDate, status }. Status is done, skip, or miss.
Updates the streak counter and triggers badge evaluation.
Prompts
POST /api/prompts
Saves a daily prompt response. Body: { promptType, content, logDate }. Prompt type is gratitude, creativity, or reflection.
GET /api/prompts
Returns all prompt logs for the authenticated user, paginated by date.
Concepts
GET /api/concepts
Returns all concepts for the current season type, including whether each is locked/completed for the current user.
POST /api/concepts/select
Starts a concept month. Body: { conceptId }.
Server enforces the order-6 lock: if the concept has order === 6, the server verifies that the other 5 of that season type are completed before allowing the start.
POST /api/concepts/abandon
Abandons the current active concept month. Sets status to abandoned. No body required.
POST /api/concepts/complete
Marks the active concept month as completed after verifying badge unlock conditions.
Badges
GET /api/badges
Returns all badges for the current season type, with earned/unearned status for the authenticated user.
Coach
GET /api/coach
Returns the current week's coach messages (brief + conversation).
POST /api/coach
Sends a user message and streams a Claude response. Appends both turns to CoachMessage.
Cron
POST /api/cron/score
Recalculates scorePct for all active UserSeason records. Protected by x-cron-secret header.
POST /api/cron/coach-brief
Generates a weekly brief for every user with an active season. Protected by x-cron-secret header. Deduplicates by weekKey — will not regenerate if a brief already exists for the current week.
Auth pattern (all protected routes)
const supabase = createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return NextResponse.json({ data: null, error: 'Unauthorized' }, { status: 401 })
const dbUser = await prisma.user.findUnique({ where: { email: user.email! } })
const userId = dbUser?.id ?? user.id
userId from user.id is never trusted directly — it is always resolved via the DB user record to ensure consistency.
Response format
// Success
return NextResponse.json({ data: result, error: null })
// Error
return NextResponse.json({ data: null, error: { message: '...' } }, { status: 400 })