2026-09-03 21:01:52 +03:30
# 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`
2026-09-22 07:24:40 +03:30
`prisma:seed` upserts the 11 course sessions and 9 assignment definitions. Server startup also upserts both as a safety net.
2026-09-03 21:01:52 +03:30
## 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) |
2026-09-22 07:24:40 +03:30
| `npm run prisma:seed` | Upsert 11 `course_sessions` + 9 assignment definitions |
2026-09-03 21:01:52 +03:30
## 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>`
- Token stored hashed in `auth_sessions` ; revoke on logout or expiry
2026-09-21 08:00:24 +03:30
- Profile age must be **at least 12** (no upper age limit)
2026-09-03 21:01:52 +03:30
- 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` .
2026-09-09 01:31:24 +03:30
`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.
2026-09-03 21:01:52 +03:30
## 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)