Core API of the SuperProfe / Innova EdTech platform: procedural math-error detection for the Chilean school curriculum. Live in production at https://api.superprofes.app.
NestJS 11 · TypeScript strict · Prisma 7 · Supabase (Auth + Postgres) · MongoDB Atlas · AWS Lambda + SQS + S3 · Serverless Framework
- 1. Overview
- 2. Architecture
- 3. Tech stack
- 4. Domain and theoretical foundation
- 5. Repository structure
- 6. Data model
- 7. Environment variables
- 8. Local setup
- 9. Testing and coverage
- 10. API surface
- 11. Production deployment
- 12. Cost and killswitches
- 13. Privacy and compliance
- 14. Methodology and workflow
- 15. License
This repository is the serverless backend that orchestrates the whole SuperProfe flow. A teacher uploads a worksheet (PDF), the system extracts each question and proposes a step-by-step solution key; the student solves it and uploads a photo of their handwritten work; the system transcribes the photo, aligns it with the key and classifies the specific procedural error (not just right/wrong), then updates a per-student, per-topic mastery profile and raises alerts for the teacher.
| Responsibility | Mechanism |
|---|---|
| Authenticate every request | Supabase JWT (JWKS, RS256) guard + role claims |
| Ingest student attempts (digital or photo) | POST /attempts with ValidationPipe + typed DTOs |
| Classify the procedural error | In-process Rule Engine (Strategy + Factory, <5 ms) |
| Update the student's mastery probability | BKT closed-form Bayesian update |
| Route unclassified errors to the LLM | SQS Standard → innova-ai-engine |
| Ingest teacher worksheets (PDF) | POST /guides presigned upload → SQS guide-ingest |
| Generate exercises / grade submissions | SQS queues consumed by innova-ai-engine workers |
| Persist telemetry events | SQS FIFO → MongoDB Atlas + S3 |
| Expose data to the teacher dashboard | GET /alerts, GET /mastery/:studentId, etc. |
| Recommend adaptive practice | Fisher-information item picker (IRT) |
The heavy AI work (PDF extraction, OCR, LLM classification, BKT/IRT calibration, exercise generation) runs in
the innova-ai-engine repo. This backend invokes it asynchronously through SQS and shared S3 buckets.
flowchart TD
subgraph CLIENTS["Client apps (innova-clients)"]
WEB["Teacher / student / parent web (Next.js)"]
MOBILE["Practice & parent apps (Expo)"]
LANDING["Landing (Astro)"]
end
subgraph API["API layer"]
AGW["API Gateway (api.superprofes.app)"]
GUARD["Supabase JWT guard (JWKS RS256) + RolesGuard"]
end
subgraph BACKEND["innova-backend-serverless (Lambda: api)"]
ATTEMPTS["Attempts module"]
RULE["Rule Engine (Strategy + Factory, <5ms)"]
BKT["Mastery service (closed-form BKT)"]
GUIDES["Guides + GuideSubmissions"]
ASSIGN["Assignment + Practice"]
ALERTS["Alerts module"]
ADMIN["Admin (catalog)"]
end
subgraph BROKERS["AWS SQS"]
FIFO["attempt-stream.fifo"]
LLM["llm-classify-queue"]
OCR["ocr-queue"]
REPRO["attempt-reprocess-queue"]
GING["guide-ingest-queue"]
SOLG["solution-generation-queue"]
SUBG["submission-grade-queue"]
end
subgraph AI["innova-ai-engine (Python Lambdas)"]
TPW["Telemetry persister"]
LCW["LLM classifier"]
OCW["OCR worker"]
GIW["Guide ingest"]
SGW["Solution generator"]
SUBW["Submission grader"]
CAL["Nightly BKT/IRT calibrators"]
end
subgraph STORAGE["Storage"]
PG[("Supabase Postgres (Prisma)")]
MONGO[("MongoDB Atlas (telemetry)")]
S3G[("S3 guides")]
S3S[("S3 submissions")]
end
EXT_ANTH["Anthropic Claude (Haiku/Sonnet)"]
EXT_GEM["Google Gemini"]
WEB & MOBILE & LANDING --> AGW --> GUARD
GUARD --> ATTEMPTS & GUIDES & ASSIGN & ALERTS & ADMIN
ATTEMPTS --> RULE
RULE -->|classified| BKT --> PG
RULE -->|UNCLASSIFIED| LLM
ATTEMPTS --> FIFO
GUIDES -->|presigned PUT| S3G --> GING
GUIDES --> SOLG
GUIDES --> SUBG
FIFO --> TPW --> MONGO
LLM --> LCW --> EXT_ANTH
OCR --> OCW --> EXT_GEM
GING --> GIW
SOLG --> SGW --> EXT_ANTH
SUBG --> SUBW --> EXT_ANTH
SUBW --> REPRO
LCW & GIW & SGW & SUBW & CAL --> PG
Attempt ingestion sequence:
sequenceDiagram
participant APP as Client app
participant AGW as API Gateway
participant CTL as AttemptsController
participant RE as RuleEngine
participant M as MasteryService
participant SF as SQS FIFO
participant SL as SQS LLM
participant PG as Postgres
APP->>AGW: POST /attempts {studentId, exerciseId, steps, finalAnswer}
AGW->>CTL: Supabase JWT validated → CreateAttemptDto
CTL->>RE: classify(steps, exercise)
alt CLASSIFIED
RE-->>CTL: {errorTag, confidence, source:"rule"}
CTL->>M: updateBkt(studentId, topicId, isCorrect)
M->>PG: upsert StudentTopicMastery
CTL->>SF: publishFifo(attempt telemetry)
CTL-->>APP: 201 {attemptId, isCorrect, errorTag, pKnown}
else UNCLASSIFIED
RE-->>CTL: {errorTag:"UNCLASSIFIED"}
CTL->>SL: publishStandard(attemptId)
CTL-->>APP: 201 {attemptId, isCorrect, errorTag:"UNCLASSIFIED"}
end
Formal UML diagrams (components, lollipop/socket interfaces, NFR notes) live in
../docs/drawio/.
| Layer | Technology | Why |
|---|---|---|
| Language | TypeScript (strict) | noImplicitAny, strictNullChecks, exactOptionalPropertyTypes |
| Framework | NestJS 11 | DI-first, modular, Guards + Interceptors |
| ORM | Prisma 7 (@prisma/adapter-pg + pg) |
Versioned migrations, generated types, serverless-friendly |
| Relational DB | Supabase Postgres | Managed Postgres; transaction pooler for serverless |
| Document DB | MongoDB Atlas | Raw, schema-less attempt telemetry |
| Messaging | AWS SQS (FIFO + Standard) | Durable, decouples expensive AI work |
| Object storage | AWS S3 | Worksheet PDFs and submission photos |
| Auth | Supabase Auth (JWKS, RS256) | JWT validation against Supabase JWKS; role claims |
| Resend | Password recovery and account notifications | |
| Validation | class-validator / class-transformer + Zod | Typed DTOs, no raw req.body |
| Cloud | AWS Lambda + API Gateway | Pay-per-request, zero idle cost |
| Deploy | Serverless Framework 3 (esbuild) | One service, multiple functions, custom domain |
| Logging | Pino (nestjs-pino) |
Structured JSON logs with trace ids |
| API docs | Swagger (@nestjs/swagger) |
OpenAPI UI at /docs |
| Tests | Jest + Supertest | Unit + E2E, coverage gate ≥75% |
| Package manager | pnpm 9 | Workspace protocol, disk efficiency |
The classification pipeline has four layers:
Layer 1 — Rule Engine (synchronous, <5 ms). Based on Brown & VanLehn (1980) "Repair Theory": procedural
arithmetic errors are systematic and catalogable. Implemented as one Strategy class per topic wired by a
Factory (topic.code → RuleStrategy). Deterministic subdomains (integer/fraction/decimal operations,
ratios/percentages, linear equations, powers/roots) are classified here. Target coverage: ≥75% of real
attempts classified as non-UNCLASSIFIED.
Layer 2 — BKT online update (synchronous, <1 ms). Based on Corbett & Anderson (1995). Closed-form
Bayesian update of pKnown per (student, topic):
P(known | obs=1) = (1-pSlip)·pKnown / [(1-pSlip)·pKnown + pGuess·(1-pKnown)]
P(known | obs=0) = pSlip·pKnown / [pSlip·pKnown + (1-pGuess)·(1-pKnown)]
P(Ln) = P(known | obs) + (1 - P(known | obs))·pTransit
Default init pL0=0.30, pT=0.10, pS=0.10, pG=0.20. Nightly recalibration runs in innova-ai-engine and
writes the parameters back to Postgres.
Layer 3 — IRT 2PL (nightly batch). Based on Lord (1980). The next exercise is chosen by maximizing Fisher
information a²·P(θ)·(1−P(θ)) at the student's current θ. Calibration runs in innova-ai-engine.
Layer 4 — LLM async classification. Attempts marked UNCLASSIFIED are pushed to SQS and classified by
Claude in innova-ai-engine against a proprietary taxonomy of 2,600+ procedural errors aligned to the Chilean
curriculum, with prompt caching and forced structured output.
Guides pipeline (v9). Beyond single attempts, the backend drives the teacher-worksheet flow: PDF upload
(guide-ingest) → solution-key generation (solution-generation) → student photo grading
(submission-grade) → reprocessing into attempts (attempt-reprocess). Each stage is an SQS queue consumed
by an innova-ai-engine worker.
innova-backend-serverless/
├── src/
│ ├── main.ts # local dev entry (nest start)
│ ├── lambda.ts # Lambda entry (@vendia/serverless-express)
│ ├── app.module.ts
│ ├── modules/
│ │ ├── auth/ # Supabase JWT strategy, RolesGuard, @CurrentUser, email (Resend)
│ │ ├── attempts/ # POST /attempts → rule engine → BKT → SQS
│ │ │ └── rule-engine/ # Strategy + Factory + per-topic strategies
│ │ ├── mastery/ # GET /mastery/:studentId — BKT state
│ │ ├── items/ # exercise bank + IRT params
│ │ ├── skills/ # topic/skill catalog
│ │ ├── guides/ # teacher worksheets: upload, ingest, solution review
│ │ ├── guide-submissions/ # student photo submissions + grading status
│ │ ├── assignment/ # Assignment generation (manual + recommender)
│ │ ├── practice/ # Fisher-information practice picker
│ │ ├── alerts/ # teacher alerts CRUD
│ │ ├── classrooms/ # courses, invites, enrollment
│ │ ├── parent/ # parent links + child summaries
│ │ └── admin/ # error-catalog admin (keyset paginated, @Roles ADMIN)
│ ├── adapters/ # SQS, S3, Anthropic/Gemini, math-OCR ports
│ ├── infrastructure/
│ │ ├── database/ # PrismaService (serverless-safe singleton)
│ │ └── workers/ # SQS/S3/cron Lambda handlers
│ └── shared/ # interceptors, filters, middleware (trace id), config
├── prisma/
│ ├── schema.prisma # full v9 schema (see §6)
│ ├── migrations/
│ └── seed.ts # demo school, courses, students, exercises
├── scripts/
│ ├── import-error-catalog.ts # import ai-engine taxonomy → ErrorTag
│ ├── codegen-error-tags.ts # generate rule-engine error enums from catalog
│ ├── seed-supabase-auth.ts # create demo auth users (Admin REST, idempotent)
│ └── local-reprocess-consumer.ts # local SQS consumer for the attempt-reprocess loop
├── test/ # E2E (jest-e2e.json)
├── docker-compose.yml # MongoDB 7 + LocalStack (SQS/S3) for local dev
├── serverless.yml # 6 Lambda functions + SQS/S3 resources + custom domain
├── .env.example
└── README.md
Postgres (Prisma, prisma/schema.prisma). The v9 schema is curriculum-first and multi-subject:
- Identity & roles:
User(supabaseUid),Teacher,Student,Parent,ParentLink. - Org & courses:
Organization,School,Course,CourseTeacher,Enrollment,ClassroomInvite. - Curriculum:
Subject,Curriculum,OfficialOACode,Unit,Topic,TopicPrerequisite,Domain,Subdomain. - Exercises & work:
Exercise(source: SYSTEM | TEACHER_AUTHORED | LLM_GENERATED),Assignment,AssignmentTarget,AssignmentExercise,Attempt,AttemptStep,AttemptErrorReport. - Mastery & alerts:
StudentTopicMastery(withtrend7d),TeacherAlert,ErrorTag(source of truth for error types, synced with the ai-engine taxonomy). - Guides:
Guide,GuideQuestion,GuideSolution,GuideSubmission. - Integrations & cost:
SchoolIntegration,ExternalIdMap(e.g. Google Classroom roster),CostEvent(per-operation AI cost accounting).
Enums: ErrorSource, ErrorStatus, ErrorSeverity, GuideStatus, GuideQuestionStatus,
SolutionSource, SubmissionStatus.
MongoDB (Mongoose). Raw, high-volume, schema-less telemetry: attempt events (keystrokes / intermediate steps for replay and debugging) and AI job audit records (request, response, cost tracking).
Template in .env.example. Never commit .env. Config is validated at boot via @nestjs/config.
| Variable | Description | Required |
|---|---|---|
DATABASE_URL |
Supabase Postgres connection string. Prod (serverless): transaction pooler :6543 + ?pgbouncer=true&connection_limit=1. Local: local Postgres or Supabase. |
✅ |
MONGODB_URI |
MongoDB Atlas / local telemetry connection string | ✅ |
SUPABASE_URL |
https://<project>.supabase.co |
✅ |
SUPABASE_SERVICE_ROLE_KEY |
Server-only admin key (bypasses RLS) | ✅ |
SUPABASE_ANON_KEY |
Public anon key | optional |
AWS_REGION |
AWS region of the resources (us-east-1) |
✅ |
SQS_ATTEMPT_STREAM_URL |
FIFO telemetry queue | ✅ |
SQS_LLM_CLASSIFY_URL |
LLM classification queue | ✅ |
SQS_OCR_QUEUE_URL |
OCR queue | ✅ |
SQS_ATTEMPT_REPROCESS_URL |
OCR → attempts reprocess loop | ✅ |
SQS_GUIDE_INGEST_URL |
v9 guide ingest queue | prod |
SQS_SOLUTION_GEN_URL |
v9 solution generation queue | prod |
SQS_SUBMISSION_GRADE_URL |
v9 submission grading queue | prod |
S3_GUIDES_BUCKET / S3_SUBMISSIONS_BUCKET |
S3 buckets for PDFs / photos | prod |
GUIDES_PRESIGNED_PUT_TTL / GUIDES_PRESIGNED_GET_TTL |
Presigned URL TTLs (seconds) | optional |
RESEND_API_KEY / RESEND_FROM_EMAIL |
Email provider (required for password recovery in prod) | prod |
PUBLIC_APP_URL / PUBLIC_API_URL |
Public URLs used in emails and links | prod |
CORS_ORIGINS |
Comma-separated browser origins (e.g. https://superprofes.app,https://app.superprofes.app) |
prod |
ANTHROPIC_API_KEY / GEMINI_API_KEY |
AI providers (only for OCR/LLM features) | prod |
LOG_LEVEL |
debug / info / warn |
optional (default info) |
- Node.js ≥20 (via
nvm) - pnpm ≥9 (
corepack enable && corepack prepare pnpm@latest --activate) - Docker + Docker Compose v2
- A Supabase project (for Auth + Postgres) or a local Postgres instance
# 1. Install dependencies
pnpm install
# 2. Environment
cp .env.example .env
# edit .env: Supabase keys, DATABASE_URL, MONGODB_URI, queue URLs (LocalStack), AI keys (optional)
# 3. Start local infra: MongoDB 7 + LocalStack (SQS/S3)
docker compose up -d
# 4. Apply migrations + seed (run these yourself; see note below)
pnpm prisma migrate dev
pnpm prisma db seed # demo school, courses, students, exercises
pnpm seed:full # seed + import the ai-engine error catalog
# 5. Dev server (hot reload)
pnpm start:dev # → http://localhost:3000
# 6. Verify
curl http://localhost:3000/health # 200 OK
# Swagger UI → http://localhost:3000/docs
docker-compose.ymlprovides MongoDB and LocalStack (SQS/S3). Postgres is expected from Supabase or a local instance — pointDATABASE_URLaccordingly (the.env.exampleshows a local Postgres on:5433). LocalStack lets you exercise the SQS/S3 flows without real AWS.
pnpm seed:auth (guarded by ALLOW_SEED=1 + SEED_DEMO_PASSWORD) creates the demo identities in Supabase
Auth idempotently via the Admin REST API, so the local DB and Supabase Auth stay in sync.
The OCR → attempts loop needs a consumer. In local dev run pnpm consume:reprocess to drain the
attempt-reprocess queue against LocalStack.
pnpm start:dev # hot reload
pnpm build # nest build
pnpm prisma studio # DB GUI
pnpm prisma migrate dev --name <name> # new migration
docker compose logs -f # infra logs
docker compose down -v # stop and wipe local datapnpm test # unit
pnpm test:cov # coverage (gate ≥75%)
pnpm test:e2e # E2E (requires a test DB; see test/jest-e2e.json)
pnpm test:watch # watch modeKey suites: rule-engine strategies (one golden case per error type), MasteryService.updateBkt (property
tests: pKnown ∈ [0,1], monotonic under repeated correct answers), AttemptsController E2E (POST attempt →
DB row + SQS message), telemetry/reprocess consumers (mock SQS events). Coverage report at
coverage/lcov-report/index.html.
REST, JSON, all routes behind the Supabase JWT guard except /health. Browse the full, always-current
contract in Swagger UI at /docs (dev). Representative endpoints:
| Method | Path | Description | Auth |
|---|---|---|---|
| GET | /health |
Healthcheck | public |
| POST | /attempts |
Ingest an attempt (digital or post-OCR) | JWT |
| GET | /attempts/:id/status |
Poll grading/classification status | JWT |
| GET | /mastery/:studentId |
Current BKT state per topic | JWT |
| GET | /skills · /items |
Topic catalog / exercise bank | JWT |
| POST | /guides |
Create a guide + presigned PDF upload | JWT (teacher) |
| GET | /guides/:id |
Guide + extracted questions + solution key | JWT (teacher) |
| POST | /items/generate |
Enqueue LLM exercise generation | JWT (teacher) |
| POST | /practice/assign |
Create an assignment | JWT (teacher) |
| GET | /alerts · PATCH /alerts/:id/resolve |
Teacher alerts | JWT (teacher) |
| GET/POST | /classrooms/* |
Courses, invites, join | JWT |
| GET | /admin/error-tags · PATCH status |
Error-catalog admin | JWT (ADMIN) |
Lambda functions (serverless.yml): api (HTTP), telemetryWorker (SQS FIFO → Mongo/S3),
llmClassifierWorker, ocrWorker, alertGenerator (cron), attemptReprocessWorker.
Production runs on AWS account 751871643325, region us-east-1, fronted by the custom domain
api.superprofes.app. The authoritative step-by-step runbook is ../docs/DEPLOY_RUNBOOK.md.
- Serverless Framework (
serverless.yml) deploys one service with six functions plus the SQS queues, S3 buckets and custom domain mapping (provider.runtime: nodejs20.x, esbuild bundling). This stack owns the SQS queues and S3 buckets thatinnova-ai-enginelater consumes, which is why the deploy order is backend → ai-engine → clients. - CI/CD:
.github/workflows/ci.ymlruns type-check + lint + tests on every PR (coverage gate)..github/workflows/deploy.ymlruns on merge tomainand deploys to AWS..github/workflows/seed-prod.ymlis a manual, dispatch-only workflow to seed the demo identities in prod.
# Local/manual deploy (CI does this automatically on merge to main)
pnpm build
pnpm prisma migrate deploy
npx serverless deploy --stage prodDATABASE_URLpooler: the backend (Prisma serverless) must use the Supabase transaction pooler (:6543) with?pgbouncer=true&connection_limit=1. Theinnova-ai-enginemust use the session pooler (:5432) instead, becauseasyncpgbreaks with the transaction pooler's prepared statements.CORS_ORIGINS: browser origins only (https://superprofes.app,https://app.superprofes.app). Do not addai.superprofes.app(it is server-side Lambda, it does not make CORS requests).- Secrets live in GitHub Actions repository secrets:
SUPABASE_URL/ANON_KEY/SERVICE_ROLE_KEY,DATABASE_URL,MONGODB_URI,AWS_ACCESS_KEY_ID/SECRET_ACCESS_KEY/REGION,ANTHROPIC_API_KEY,GEMINI_API_KEY,RESEND_API_KEY/FROM_EMAIL,PUBLIC_API_URL/APP_URL,SEED_DEMO_PASSWORD. - Verify:
curl -s https://api.superprofes.app/health→200 OK.
After merging the release PR (develop → main) and a successful deploy, back-merge main → develop and
create the GitHub release/tag (see ../docs/DEPLOY_RUNBOOK.md §5–6).
The architecture keeps marginal cost near zero when idle (pay-per-request Lambda, pooled serverless
Postgres). The dominant variable cost is AI inference (OCR + LLM), tracked per operation in the CostEvent
table. Billing alarms in CloudWatch flip SSM flags (LLM_PAUSED, OCR_PAUSED) that the workers check before
calling a provider; when paused, messages go to the DLQ or fall back to digital-only input. Email cost is
negligible; Resend is the single provider to avoid configuration branches.
- No PII reaches the LLM or OCR providers: SQS messages carry only
student_uuid. - Submission photos use random UUID filenames and are purged by an S3 lifecycle policy.
- Every request requires a Supabase JWT; there is no anonymous access.
- Each
Attemptrecords itsclassifierSource(rule/llm/human) for full auditability. - Parental consent is recorded in
ParentLinkbefore photo uploads are enabled. - Minors' data is not shared with third-party analytics services. Aligned with COPPA and Chile's Law 21.180.
The project follows GSD/BMAD with declared AI-agent usage. Living artifacts are in ../docs/
(roadmap.md, milestones.md, requirements.md, architecture.md) and per-session AI logs in
../docs/ai-logs/.
Gitflow: develop is the integration branch; main is protected (PR + review + green CI, no force-push).
Commits follow Conventional Commits in English (feat(attempts): ..., fix(bkt): ...). Quality gates
that block merge: pnpm build, pnpm lint, pnpm test:cov (≥75%), pnpm test:e2e.
Innova — Team 23. Internal GPL-3.0 license.