This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
cd frontend
npm install # install deps
npm run dev # dev server at http://localhost:3000
npm run build # production build
npm run lint # ESLintcd backend
python3 -m venv venv && source venv/bin/activate
pip install -r requirements.txt
# Run dev server
uvicorn app.main:app --reload --port 8000
# Database migrations (Alembic)
alembic upgrade head # apply pending migrations
alembic revision --autogenerate -m "message" # generate a new migrationPrepStudio is a split monorepo: frontend/ (Next.js 14 App Router) + backend/ (FastAPI async) + an admin/ directory. There is no monorepo tooling — each side is run independently.
Two-token system to avoid Firebase verification on every request:
- Frontend calls Firebase Auth (Google OAuth or email/password)
- On login,
authStore.tsimmediately POSTs the Firebase ID token toPOST /auth/token - Backend verifies it once against Firebase Admin SDK, issues its own short-lived HS256 JWT (
jwt_utils.py) - All subsequent API calls use the backend JWT — Firebase is never touched again unless the JWT expires (~24h)
api.tsAxios interceptor callsgetBackendToken()before every request; returns cached JWT or silently re-exchanges
AUTH_BYPASS=true (the default in config.properties) skips all auth for local development.
core/config.py—Settingsclass reads fromconfig.propertiesfirst, then env vars win. Config file is local-only; production uses env vars.core/database.py— Async SQLAlchemy withNullPool(required for Nile DB / PgBouncer transaction-mode pooling). Never add a connection pool here.core/auth.py—get_current_userFastAPI dependency. Tries backend JWT first (fast path, no network), falls back to Firebase token verification (legacy/first-request path).models/all.py— All SQLAlchemy ORM models in a single file:User,Plan,PlanDay,Topic,Article,Interview.schemas/all.py— All Pydantic request/response schemas.routers/— One file per resource:plans.py,topics.py,articles.py,interviews.py,auth.py.services/gemini_service.py— Gemini 2.5 Flash calls (plan generation, topic content, article refinement, interview evaluation).services/elevenlabs_service.py— Creates ephemeral ElevenLabs Conversational AI agents per session, returns a signed WebSocket URL. Agents are created fresh each call and not reused.services/plan_service.py— Orchestrates plan creation: calls Gemini, persistsPlan+PlanDay+Topicrows.
app/— Next.js App Router. Routes:/(landing),/dashboard,/new,/plan/[id],/plan/[id]/day/[dayId]/topic/[topicId],/plan/[id]/interview,/plan/[id]/topic/[topicId]/article,/privacy,/terms. Auth routes in(auth)/.store/authStore.ts— Zustand store managing Firebase user state and backend JWT lifecycle. ThegetBackendToken()method is the single source of truth for token access.store/interviewStore.ts— Zustand store for voice interview session state.hooks/useElevenLabsConversation.ts— Wraps@11labs/react'suseConversation. DetectsINTERVIEW_COMPLETEin AI messages and firesonInterviewCompletecallback with the full transcript string.hooks/useVoice.ts— Simpler voice hook for the Audio Lesson (Nova tutor) flow.lib/api.ts— Axios instance with auth interceptor. Import this for all API calls.lib/firebase.ts— Firebase initialization. Importauthfrom here.
Both voice features follow the same pattern:
- Frontend POSTs to backend to start a session
- Backend calls Gemini (interviews only) to generate questions, then builds a system prompt
- Backend calls
ElevenLabs Agents APIto create an agent, gets a signed WebSocket URL - Backend returns the signed URL to frontend
- Frontend connects via
useElevenLabsConversation/useVoicehooks using@11labs/react
The INTERVIEW_COMPLETE sentinel is a literal string the Alex agent is instructed to say; the frontend hook watches for it to trigger transcript evaluation.
PostgreSQL via Nile DB (production). All models are in backend/app/models/all.py. Alembic manages schema migrations — the single migration file is at backend/alembic/versions/. At startup: RUN_MIGRATIONS=true runs Alembic; otherwise create_all is used (safe for dev).
Topic.content is lazily generated: GET /topics/:id generates and caches Gemini content on first request. Subsequent calls return the cached content column value.
- Backend local dev:
backend/config.properties(copy fromconfig.properties.example). Key settings:AUTH_BYPASS,DATABASE_URL,GEMINI_API_KEY,ELEVENLABS_API_KEY, Firebase credentials,JWT_SECRET. - Frontend local dev:
frontend/.env.local. Key settings:NEXT_PUBLIC_API_URL, Firebase client-side config (NEXT_PUBLIC_FIREBASE_*). - Production: All settings via environment variables;
config.propertiesis not present.
- Frontend: Vercel
- Backend: Custom host with
RUN_MIGRATIONS=trueenv var set to trigger Alembic on startup backend/vercel.jsonexists for optional Vercel serverless backend deployment