# Novya Backend Independent HTTP API for the Novya learning PWA. Authenticated progress, PCQ-24 assessments, course sessions, chat, and AI coach run through this service; MySQL is the source of truth. ## Tech Stack - **Runtime:** Node.js 20+ + TypeScript (ESM) - **HTTP:** Hono - **Database:** MySQL 8 via Prisma (`backend/prisma/schema.prisma`) - **Validation:** Zod - **Auth:** Argon2 passwords, opaque Bearer sessions (SHA-256 hashed at rest) - **Logging:** Pino - **AI:** OpenAI-compatible client (default GapGPT via `OPENAI_BASE_URL`) ## Prerequisites - Node.js 20+ - npm - MySQL 8+ (local or `docker compose -f docker-compose.dev.yml up -d`) ## Getting Started ```bash cd backend npm install cp .env.example .env # docker compose -f docker-compose.dev.yml up -d npm run prisma:migrate:dev npm run prisma:seed npm run dev ``` API default: `http://localhost:3001` PWA (CORS): `http://localhost:5173` `prisma:seed` upserts the 11 course sessions. Server startup also upserts them as a safety net. ## Scripts | Script | Description | |--------|-------------| | `npm run dev` | Dev server with hot reload | | `npm run build` | Compile TypeScript → `dist/` | | `npm run start` | Run compiled server | | `npm run test` | Domain + e2e API tests | | `npm run lint` | ESLint | | `npm run typecheck` | `tsc --noEmit` | | `npm run prisma:generate` | Generate Prisma Client | | `npm run prisma:validate` | Validate schema | | `npm run prisma:migrate:dev` | Dev migrations | | `npm run prisma:migrate:deploy` | Apply migrations (prod) | | `npm run prisma:seed` | Upsert 11 `course_sessions` | ## Project Structure ``` backend/ ├── prisma/ │ ├── schema.prisma # Active schema (identity + learning domain) │ ├── seed.ts # Catalog seed (11 sessions) │ └── migrations/ ├── src/ │ ├── app.ts / server.ts # App factory + listen / shutdown │ ├── config/ # Env (Zod) │ ├── lib/ # prisma, logger, response, clientIp │ ├── middleware/ # requestId, CORS, rate-limit, auth, errors │ ├── routes/ # health + module mounts │ ├── modules/ │ │ ├── auth/ # register, login, guest, me, logout │ │ ├── users/ # profile name/age │ │ ├── journey/ # learning step + permissions │ │ ├── assessments/ # PCQ-24 attempts + scoring │ │ ├── sessions/ # catalog, progress, complete │ │ ├── chat/ # chat sessions + messages │ │ └── ai/ # config, runs, coach prompt/client │ └── shared/ # errors, types, validation, constants ├── tests/ ├── docker-compose.dev.yml └── .env.example ``` Root `prisma/schema.prisma` is a **legacy design artifact** — see `../prisma/README.md`. Use only `backend/prisma/`. ## Environment Variables | Variable | Required | Default | Description | |----------|----------|---------|-------------| | `NODE_ENV` | No | `development` | `development`, `test`, `production` | | `PORT` | No | `3001` | HTTP port | | `HOST` | No | `0.0.0.0` | Bind host | | `API_VERSION` | No | `1.0.0` | Returned by `/version` | | `BUILD_TIMESTAMP` | No | — | Optional build stamp for `/version` | | `DATABASE_URL` | **Yes** | — | `mysql://...` (use `novya`; keep `novya_test` for tests only) | | `LOG_LEVEL` | No | `info` | Pino level | | `CORS_ORIGIN` | No | `http://localhost:5173` | Comma-separated origins | | `TRUST_PROXY` | No | `false` | Proxy-aware client IP | | `RATE_LIMIT_WINDOW_MS` | No | `60000` | Rate-limit window | | `RATE_LIMIT_MAX` | No | `100` | Max requests / window / IP | | `AUTH_RATE_LIMIT_MAX` | No | `20` | Cap for register/login/guest | | `AUTH_SESSION_TTL_HOURS` | No | `168` | Opaque session TTL (7 days); no refresh tokens | | `OPENAI_API_KEY` | For AI | `""` | Backend-only; empty → AI routes return 503 | | `OPENAI_BASE_URL` | No | `https://api.gapgpt.app/v1` | OpenAI-compatible base URL | | `OPENAI_MODEL` | No | `gpt-4o-mini` | Model id | | `OPENAI_ORG_ID` | No | — | Optional OpenAI org header | | `OPENAI_PROJECT_ID` | No | — | Optional OpenAI project header | Do **not** put `VITE_*` secrets in the backend. ## Auth - Bearer token from register/login/guest → header `Authorization: Bearer ` - Token stored hashed in `auth_sessions`; revoke on logout or expiry - Profile age must be **at least 12** (no upper age limit) - One PCQ attempt per user per phase (`UNIQUE(userId, phase)`) ## Learning journey (`currentStep`) Timestamps are authoritative. Derived steps: `registered` → `pre_test_done` / `training_in_progress` → `training_completed` → `completed` Permissions on `GET /journey`: `canTakePreTest`, `canOpenTraining`, `canTakePostTest`, `isCompleted`. `canTakePostTest` is true after pre-test when either training is fully completed **or** the learner has completed sessions 1–10 (reached Session 11), and the post-test is not yet submitted. ## API Endpoints ### Health | Method | Path | Auth | Description | |--------|------|------|-------------| | `GET` | `/health` | No | Liveness | | `GET` | `/ready` | No | Readiness (MySQL) | | `GET` | `/version` | No | Version info | ### Auth & users | Method | Path | Auth | Description | |--------|------|------|-------------| | `POST` | `/auth/register` | No | Email/password (min 8) | | `POST` | `/auth/login` | No | Login | | `POST` | `/auth/guest` | No | Guest user + session | | `GET` | `/auth/me` | Yes | Current user | | `POST` | `/auth/logout` | Yes | Revoke session | | `GET` | `/users/me` | Yes | User + profile | | `PATCH` | `/users/me/profile` | Yes | Name + age | ### Journey & assessments | Method | Path | Auth | Description | |--------|------|------|-------------| | `GET` | `/journey` | Yes | Step + permissions | | `GET` | `/assessments/scales/pcq24` | Yes | Scale metadata | | `POST` | `/assessments/attempts` | Yes | Submit pre/post | | `GET` | `/assessments/attempts` | Yes | History | | `GET` | `/assessments/attempts/latest?phase=` | Yes | Latest by phase | | `GET` | `/assessments/summary` | Yes | Pre/post + journey | ### Sessions (catalog) | Method | Path | Auth | Description | |--------|------|------|-------------| | `GET` | `/sessions` | Yes | Catalog + unlock/complete flags | | `GET` | `/sessions/progress` | Yes | Progress summary | | `GET` | `/sessions/:order` | Yes | One session | | `POST` | `/sessions/:order/complete` | Yes | Mark complete (idempotent) | ### Chat & AI | Method | Path | Auth | Description | |--------|------|------|-------------| | `GET` | `/chat/sessions` | Yes | List chats (`?sessionOrder=` optional) | | `POST` | `/chat/sessions` | Yes | Create/reuse chat for a session order | | `GET` | `/chat/sessions/:id/messages` | Yes | Message history | | `POST` | `/chat/sessions/:id/messages` | Yes | User message → coach reply | | `POST` | `/chat/sessions/:id/start` | Yes | Opening coach turn | | `GET` | `/ai/config` | No | Public AI config (no secrets) | | `GET` | `/ai/runs` | Yes | User’s AI run log | Response envelope: ```json { "success": true|false, "data"|"error": ..., "meta": { "requestId", "timestamp" } } ``` ## Schema enums (MVP usage) | Enum | Values | Runtime usage | |------|--------|----------------| | `AttemptStatus` | `submitted`, `scored` | Always `scored`. `submitted` reserved for future drafts. | | `AiRunStatus` | `queued`, `running`, `succeeded`, `failed` | Create as `queued` → `running` → terminal | | `SessionProgressStatus` | `not_started`, `in_progress`, `completed` | Chat opens as `in_progress`; `not_started` is default only | | `ChatRole` | `system`, `user`, `assistant` | Only `user`/`assistant` persisted; `system` is prompt-only | ## Catalog integrity `session_progress.sessionOrder` and `chat_sessions.sessionOrder` FK → `course_sessions.order` (`ON DELETE RESTRICT`). Seed the catalog before creating progress/chat rows (`npm run prisma:seed`). ## Deferred (not in this API) OAuth, phone OTP, guest→account merge, refresh tokens, versioned PCQ tables in DB, production-grade distributed rate limiting. ## Related docs - [`BACKEND_DATABASE_ARCHITECTURE_SUMMARY.md`](../BACKEND_DATABASE_ARCHITECTURE_SUMMARY.md) — architecture overview - Auth history: `BACKEND_AUTH_FOUNDATION_REPORT.md` (may be partially historical)