Files
pwa/backend/README.md
T
ar.badieiandCursor 370ac15471 Ship Novya PWA with backend for deployable production use.
Fix production TypeScript builds and add DEPLOY.md so the app can be hosted from this repo.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-03 21:01:52 +03:30

205 lines
8.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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>`
- Token stored hashed in `auth_sessions`; revoke on logout or expiry
- Profile age must be **18–100**
- 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`.
## 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)