Ship admin auth, user overview, assessments, chats, assignment filters/export, security hardening, and a separate desktop admin shell with clearer assessment answers. Co-authored-by: Cursor <cursoragent@cursor.com>
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
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 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:
{ "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— architecture overview- Auth history:
BACKEND_AUTH_FOUNDATION_REPORT.md(may be partially historical)