interviews-ui
Purpose
interviews-ui is the web application every user of theinterviews.ai touches: it renders the marketing site, candidate/recruiter/admin dashboards, the AI interview room, the Profile Card, and plan checkout. It is a Next.js 15 / React 19 / TypeScript app, and it is the front door to four backend services — it owns presentation and routing, not business logic.
Architecture
The platform is five services; this repo is the browser-facing one. The golden rule is the three data paths — every network call from the front end takes exactly one of them:
- Java core backend (
user-management) over REST + JWT — users, meetings, subscriptions, recordings, blog, platform config. Always through the typed clients insrc/services/*, never inlinefetchin components. - Next.js
/api/*serverless routes — OpenAI-facing only (resume ATS scoring, resume match, AI mock-interview Q&A). These exist purely to keep the OpenAI key server-side. User, billing, meeting, and recording data must never flow through them. - LiveKit over WebSocket — real-time audio/video. The Java backend mints a time-limited token (
GET /api/meetings/{id}/authorize-video); the browser then connects directly to the LiveKit SFU.
Two backends sit behind the AI interview experience: video-streaming-server (Node — the legacy voice brain: STT, LLM Q&A/evaluation, TTS, avatar session minting) and bot-backend (Python + livekit-agents — the next-generation interview worker, currently in migration/eval under TI-340). The live in-call session room is a separate React repo, smart-interview-ui.
Video meeting flow
The component tree branches on NEXT_PUBLIC_VIDEO_PLATFORM_MODE: INTERNAL_V1 routes to the in-app /meeting/[meetingId] page (LiveKit WebRTC), while LEGACY redirects the user to an external join URL. In the internal path, the page calls the Java backend to authorize the user and fetch a LiveKit token, then connects over WebSocket; recording happens server-side via LiveKit Egress, and an AI agent can join the room silently to transcribe and summarize.
Key components
| Path | Owns |
|---|---|
src/app/ | App Router pages: / landing, /auth, /signup/*, /customer/* (dashboard, AI mock/bot interviews, resume optimizer, profile, subscription), /meeting/[meetingId], /admin/*, /blog |
src/app/api/ | Serverless routes — OpenAI-only (resume upload/ATS/match, AI mock-interview sessions) |
src/services/ | REST clients per domain (auth, blog, AI interview, …) — the only sanctioned way to call the Java backend |
src/store/ | Redux Toolkit slices + selectors, for cross-cutting state only (auth, user, plan entitlements) |
src/features/ | Feature-scoped modules, e.g. ai-bot-interview (the voice AI interview room, avatar routing, bot service) |
src/hooks/ | Custom hooks, e.g. useLiveKit for room lifecycle management |
src/components/ | Shared UI primitives (video grid, tracks, meeting controls) |
src/utils/ | Utilities — notably planEntitlements.ts (all plan/entitlement checks) and featureFlags.ts (video platform mode resolution) |
src/lib/ | Framework-neutral helpers |
docs/ | In-repo deep dives: SETUP, BACKEND, VIDEO_PLATFORM, AI_AGENT, AMPLIFY_ENV_SETUP, PLAN_LIMITS_AND_TESTING |
State management convention: TanStack Query for server data that doesn't need to live in Redux; Redux only for cross-cutting state.
Local development
Prerequisites: Node.js 18+, npm. For the full stack you also need Java 17+ (the user-management backend), Docker (a local LiveKit server), and optionally Python (the bot-backend AI agent) — each has its own setup guide.
npm install
# create your local env file from the template, then fill in values
cp .env.example .env.local
npm run dev:local # dev server on localhost:3000 — reads .env.local directly
Other commands you'll use:
npm run build # production build (runs scripts/check-env.js first)
npm test # vitest run; single file: npx vitest run <file-pattern>
npm run lint # ESLint (next lint)
npx tsc --noEmit # typecheck (no npm script for this)
npm run check-env # validate required env vars
npm run restore:local # restore .env.local after an env-swapped run (see Gotchas)
The UI is usable standalone for marketing pages, but anything behind login needs the Java backend running, and the meeting room additionally needs a LiveKit server (Docker image livekit/livekit-server works locally).
Environment variables
Names only — set real values in .env.local locally, and in the Amplify Console per branch for deployed environments (Amplify does not read .env files).
| Variable | Purpose |
|---|---|
NEXT_PUBLIC_ENVIRONMENT | Environment identifier (local / development / staging / production); highest-priority input to environment detection |
NEXT_PUBLIC_VIDEO_PLATFORM_MODE | INTERNAL_V1 (in-app LiveKit meetings) or LEGACY (redirect to external join URL) |
NEXT_PUBLIC_LIVEKIT_WS_URL | LiveKit WebSocket endpoint, e.g. <LIVEKIT_WS_URL> |
NEXT_PUBLIC_API_URL | Base URL of the Java backend REST API, e.g. <API_BASE_URL> |
NEXT_PUBLIC_APP_URL | Application base URL used by the app, e.g. <API_BASE_URL> |
NEXT_PUBLIC_JOIN_URL_BASE | Base URL used when generating meeting join links |
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY | Stripe publishable key for checkout (publishable, not secret) |
OPENAI_API_KEY | OpenAI key — server-side only, consumed by the /api/* serverless routes |
OPENAI_MODEL | OpenAI model name used by the resume/AI-interview routes |
Run npm run check-env to confirm everything required is set.
Gotchas
.envis a derived, transient file. The env-swapping scripts (dev:dev,dev:production,build:*,start:*,export:*) back up.env.localand copy the target env file over.env. The canonical sources are.env.local,.env.dev,.env.production. Onlydev:localreads.env.localdirectly with no swap. After any swapped run, runnpm run restore:local— or your local env is silently wrong.- Never route user data through
/api/*. Those serverless routes are OpenAI-facing only. User/billing/meeting/recording data goes throughsrc/services/*to the Java backend. Routing it through/api/*by accident sends user data to the wrong place. - Inline
fetchin components is banned. Every backend call goes through asrc/services/<domain>Serviceclient. - Plan checks go through
src/utils/planEntitlements.ts. Never scatterplan === 'pro'string comparisons across components. - Amplify does not read
.envfiles. Deployed env vars are configured per branch in the Amplify Console — a value that works locally can be missing in a deployed build. Seedocs/AMPLIFY_ENV_SETUP.mdin the repo. - Environment detection has a priority chain:
NEXT_PUBLIC_ENVIRONMENT→ Amplify branch name → hostname →NODE_ENV. Know which one is winning before you debug "wrong API URL" issues. NEXT_PUBLIC_VIDEO_PLATFORM_MODEforks real code paths. Components branch onINTERNAL_V1vsLEGACY— when changing video behavior, exercise both.- Tests exist. Vitest + jsdom is configured, with suites under
src/features/ai-bot-interview/__tests__/andsrc/app/components/__tests__/. (Older docs claimed "no tests" — outdated.) - Design constraints are enforced: single accent color
#7C7AED, no multi-color gradients, no emojis on AI Interview / Recruiter / Pricing / Profile Card surfaces, no fake stats.npm run buildandnpm run lintmust pass with zero warnings before work is "done".