Files
..

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 and 9 assignment definitions. Server startup also upserts both 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 + 9 assignment definitions

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.