Practice selling websites to a small business owner without the risk of a real pitch going wrong. The app roleplays an AI business owner (coffee shop, barbershop, salon, restaurant, gym, and more) with a randomized personality, budget, and set of objections, then — once you end the session — runs a second AI pass that scores your performance and returns structured, actionable coaching feedback as JSON (rapport, business discovery, confidence, handling objections, value selling, closing, plus strengths/weaknesses/missed opportunities/better responses/next practice focus).
- Monorepo: pnpm workspaces —
client/(React + Vite) andserver/(Node + Express), managed from the rootpackage.json. - Frontend: React 18, React Router, Vite, CSS Modules. Talks to Supabase directly for auth and to the backend for everything else.
- Backend: Express (ES Modules), thin controllers ->
services/->repositories/, Zod validation, Pino logging, Helmet + CORS + rate limiting. Exported as a plainapp(no.listen) so it can run locally (server/src/index.js) or as a single Vercel serverless function (api/index.js). - Database & Auth: Supabase (Postgres with Row Level Security on every table, and Supabase Auth for signup/login/session management).
- AI: Groq (OpenAI-compatible chat completions API), used in two
distinct modes:
- Roleplay mode — generates the AI business owner's in-character reply during the chat.
- Evaluation mode — generates the structured JSON coaching evaluation once a session ends.
Both modes support
AI_MOCK=1to return canned responses with no network call or API key, for offline development and CI.
Browser (React) --Supabase JS client--> Supabase Auth (signup/login/session)
|
|--HTTP (Bearer <supabase JWT>)--> Express API --> Supabase Postgres (RLS)
|
+--> Groq (roleplay / evaluation)
Sales_Coach/
api/index.js Vercel serverless entry (re-exports the Express app)
client/ React + Vite frontend
src/
pages/ Login, Register, Dashboard, PracticeSetup, Conversation, Evaluation
components/ Reusable UI (Button, Card, Modal, ScoreCard, Chart, ErrorPage, ...)
services/ supabaseClient, auth, httpClient, services/api (typed API calls)
context/ AuthContext
server/ Express backend
src/
app.js Configured Express app (no .listen)
index.js Local dev entry (app.listen)
config/ Validated env config
routes/ controllers/ Thin HTTP layer
services/ repositories/ Business logic + Supabase data access
ai/ prompts/ Groq client + roleplay/evaluation prompt builders
middleware/ auth, validate, rateLimit, security, errorHandler
supabase/
migrations/ 0001_init.sql (schema), 0002_rls.sql (RLS policies)
seed/ business_profiles.sql (10 business-type archetypes)
docs/
contracts.md Single source of truth for shapes/enums used across the app
DATABASE.md Schema, RLS policies, migration/seed instructions
API.md Full endpoint reference
activity-log.md Dated project history
.github/workflows/ci.yml Lint + test + build pipeline
vercel.json Build/output config + API rewrites for Vercel
.env Root env file (gitignored) shared by server + client (see below)
- Node.js >= 22.13
- pnpm (version pinned via the root
package.jsonpackageManagerfield) - A Supabase project (Postgres + Auth) — or skip Groq setup entirely and still exercise the DB layer against a real Supabase project
- A Groq API key (free tier, no credit card) — or set
AI_MOCK=1to run the whole app (and the test suite) fully offline with canned AI responses
-
Install dependencies from the repo root:
pnpm install
-
Create a root
.envfile (this checkout does not ship a.env.exampletemplate — create.envdirectly using the variable list below) and fill in real values:# from the repo root New-Item .env -ItemType File # Windows PowerShell # touch .env # macOS/Linux
The server loads a single root
.envviadotenv; Vite also reads it (envDir: ".."inclient/vite.config.js) and exposes only theVITE_-prefixed keys to the browser bundle. Never commit.env— it's already in.gitignore.Variable Used by Notes NODE_ENVserver development/production/testAPI_PORTserver local dev port for Express (default 3001)CORS_ORIGINserver allowed origin for the frontend (e.g. http://localhost:5173)SUPABASE_URLserver Supabase project URL SUPABASE_ANON_KEYserver Supabase anon/public key — used to build a token-scoped, RLS-respecting client per request SUPABASE_SERVICE_ROLE_KEYserver Supabase service-role key — admin client, server-only, never exposed to the client GROQ_API_KEYserver Groq API key (not required when AI_MOCK=1)GROQ_MODELserver Groq model name (e.g. llama-3.3-70b-versatile)AI_MOCKserver 1to bypass real Groq calls and use canned roleplay/evaluation responsesAI_RATE_LIMIT_WINDOW_MS/AI_RATE_LIMIT_MAXserver Rate limit window/max for /api/chatand/api/end-sessionVITE_SUPABASE_URLclient (public) Same Supabase project URL, exposed to the browser VITE_SUPABASE_ANON_KEYclient (public) Supabase anon key — safe to expose; RLS enforces access control, not secrecy of this key VITE_API_BASE_URLclient (public) Base URL the frontend calls, e.g. http://localhost:3001/apiAny
VITE_-prefixed variable ships to the browser bundle — never put a secret there. Server secrets (SUPABASE_SERVICE_ROLE_KEY,GROQ_API_KEY) must only ever appear in the non-VITE_section. -
Run the database migrations and seed data against your Supabase project — see docs/DATABASE.md for the full step-by-step (SQL Editor or Supabase CLI options), table/RLS reference, and idempotency notes. In short:
# via Supabase CLI, from the repo root supabase link --project-ref <your-project-ref> supabase db push supabase db execute --file supabase/seed/business_profiles.sql
pnpm dev # runs client + server together
pnpm dev:client # client only (Vite dev server)
pnpm dev:server # server only (nodemon)- Client: http://localhost:5173
- Server: http://localhost:3001 (API mounted at
/api, e.g. http://localhost:3001/api/health)
Ports are fixed (strictPort: true in client/vite.config.js, API_PORT in .env) to avoid
monorepo dev-server collisions.
Set AI_MOCK=1 in .env to run the full roleplay + evaluation flow with canned AI
responses — no network call, no API key required. This is also how CI runs the test suite (see
below).
pnpm test # runs every workspace's tests (pnpm -r test)
pnpm test:server # server only (Jest, ES Modules via NODE_OPTIONS=--experimental-vm-modules)
pnpm test:client # client only (Jest + React Testing Library)Client also has Playwright e2e specs (pnpm --filter ./client e2e). Lint everything with
pnpm lint (pnpm lint:fix to auto-fix).
Deployed as a single Vercel project:
- Frontend: static build of
client/(pnpm --filter ./client build->client/dist), served as the Vercel project's output directory. - Backend: the Express app is exported as a single serverless function from
api/index.js(export default app— Express apps are valid(req, res)handlers).vercel.jsonrewrites/api/*toapi/index.jsand everything else toclient/dist/index.html(SPA fallback). - Database & Auth: Supabase hosts Postgres and Auth — nothing database-related runs on Vercel; the serverless function only talks to Supabase over the network. No local-disk state is used anywhere in the backend, since the serverless filesystem is ephemeral.
Required environment variables on Vercel (Project Settings -> Environment Variables) — same
names/purposes as the local .env table above: SUPABASE_URL, SUPABASE_ANON_KEY,
SUPABASE_SERVICE_ROLE_KEY, GROQ_API_KEY, GROQ_MODEL, AI_MOCK, CORS_ORIGIN,
AI_RATE_LIMIT_WINDOW_MS, AI_RATE_LIMIT_MAX, VITE_SUPABASE_URL, VITE_SUPABASE_ANON_KEY,
VITE_API_BASE_URL (pointed at the deployed origin, e.g. https://your-app.vercel.app/api).
See docs/API.md for the full endpoint reference and docs/DATABASE.md for the schema/RLS/migrations this deployment depends on.
CI (.github/workflows/ci.yml) runs on every push/PR to main: install, lint, test (with
AI_MOCK=1 and dummy Supabase env vars so config validation passes without real secrets),
then build the client. The pipeline fails the build if any of those steps fail.
Full detail (auth, request/response shapes, error codes) in docs/API.md. Registration/login are not backend routes — they go through the Supabase JS client directly; the backend only verifies the resulting Supabase JWT.
| Method & Path | Auth | Purpose |
|---|---|---|
GET /api/health |
No | Liveness check |
GET /api/config |
No | Enum options for the Practice Setup form |
GET /api/profile |
Yes | Current user's profile |
POST /api/session |
Yes | Start a new practice session (generates a business profile) |
GET /api/session/:id |
Yes | Session detail + messages + evaluation (if ended) |
POST /api/chat |
Yes (rate-limited) | Send a seller message, get the AI owner's reply |
POST /api/end-session |
Yes (rate-limited) | End the session, get the AI coaching evaluation |
GET /api/history |
Yes | Paginated list of past sessions |
GET /api/statistics |
Yes | Aggregate skill averages + score trend |