Skip to main content

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 })

See also