Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/vruizz22/innova-backend-serverless/llms.txt

Use this file to discover all available pages before exploring further.

This page is the high-level architecture reference for Innova Backend Serverless. It covers every tier of the system — from the client apps through API Gateway and JWT validation, into the Lambda functions and SQS queues, and out to storage and the AI engine. Read this before making structural changes to the codebase, adding Lambda functions, or modifying the SQS topology.

System overview

The system is divided into three tiers that communicate through well-defined interfaces: Client apps (innova-clients monorepo). Three web/mobile surfaces built by the front-end team — a teacher/student/parent web app (Next.js), a mobile practice and parent app (Expo), and a public landing page (Astro). All three talk exclusively to api.superprofes.app; none call AI providers or databases directly. API layer. AWS API Gateway receives every HTTP request and routes it to the api Lambda function. Before any controller code runs, the Supabase JWT guard validates the RS256 token against Supabase’s JWKS endpoint, extracts the role claim, and enforces RolesGuard on protected routes. Routes decorated with @Public()GET / and all POST /auth/* endpoints (register, login, refresh, forgot-password, confirm-forgot-password) — bypass the guard. Serverless backend. One NestJS application (src/) deployed as six Lambda functions via the Serverless Framework. The api function handles all HTTP traffic. The remaining five functions are background workers triggered by SQS events, S3 events, and a cron schedule. Together they own the SQS queues and S3 buckets that innova-ai-engine consumes — this ownership constraint determines the deploy order.

Request flow

The following sequence describes what happens when a student submits a digital math attempt end-to-end:
1

Client sends POST /attempts to API Gateway

The client app (web or mobile) sends a JSON payload containing studentId, topicCode, rawSteps (an array of {expression, isFinal} objects), expectedAnswer, and studentAnswer to https://api.superprofes.app/attempts. The optional exerciseId and courseId fields may also be included.
2

Supabase JWT guard validates the RS256 token

API Gateway invokes the api Lambda. The NestJS SupabaseStrategy (passport-jwt) fetches the Supabase JWKS, verifies the token’s RS256 signature and expiry, and populates the @CurrentUser() decorator. RolesGuard then checks the role claim. Invalid or expired tokens return 401; insufficient role returns 403.
3

AttemptsController calls the Rule Engine

AttemptsController.create() deserialises the request body through CreateAttemptDto (class-validator + class-transformer with whitelist: true, forbidNonWhitelisted: true), then calls the Rule Engine service. The factory maps topicCode to the correct RuleStrategy implementation and runs synchronous pattern matching in under 5 ms.
4

Classified path — BKT update, Postgres upsert, telemetry FIFO

If the strategy returns a concrete errorTag (anything other than UNCLASSIFIED):
  • MasteryService.updateBkt() computes the closed-form Bayesian update and upserts the StudentTopicMastery row in Postgres.
  • TelemetryService publishes the raw attempt event to the attempt-stream.fifo SQS queue.
  • The controller returns 201 with { attemptId, isCorrect, errorTag, confidence, source: "rule", pKnown }.
5

Unclassified path — SQS LLM queue, immediate 201

If the rule engine returns { errorTag: "UNCLASSIFIED" }, the attempt row is written to Postgres with that tag, and attemptId is published to the llm-classify-queue SQS Standard queue. The controller still returns 201 immediately with { attemptId, isCorrect, errorTag: "UNCLASSIFIED" }. The resolved classification arrives asynchronously — clients can poll GET /attempts/:id/status.

Lambda functions

The serverless.yml defines six Lambda functions. All run on nodejs20.x with esbuild bundling.
FunctionHandlerTriggerPurpose
apisrc/lambda.handlerHTTP (API Gateway)All REST endpoints — handles every route in the NestJS app via @vendia/serverless-express
telemetryWorkersrc/infrastructure/workers/telemetry-persister.handlerSQS FIFO batch (attempt-stream.fifo)Persists raw attempt telemetry events to MongoDB Atlas and archives a copy to S3
llmClassifierWorkersrc/infrastructure/workers/llm-classifier.handlerSQS Standard (llm-classify-queue)Forwards UNCLASSIFIED attempt IDs to innova-ai-engine via Anthropic Claude; writes the resolved errorTag back to Postgres
ocrWorkersrc/infrastructure/workers/ocr-worker.handlerS3 ObjectCreated on ocr-uploads bucketTranscribes handwritten math photos; sends transcription to Gemini; publishes result to attempt-reprocess-queue
alertGeneratorsrc/infrastructure/workers/alert-generator.handlerEventBridge cron (hourly)Queries StudentTopicMastery for students whose pKnown has dropped or stagnated; upserts TeacherAlert rows
attemptReprocessWorkersrc/infrastructure/workers/attempt-reprocess.handlerSQS Standard (attempt-reprocess-queue)Converts OCR-transcribed step arrays back into POST /attempts-equivalent payloads and routes them through the rule engine
The api Lambda caches the NestJS application instance in a module-level variable (cachedServer) across warm invocations. Cold starts re-run bootstrap() and recreate the Prisma and Mongoose connections. If bootstrap fails, subsequent invocations return a structured 500 rather than crashing silently.

Storage layers

The system uses three storage technologies with distinct responsibilities: Supabase Postgres (Prisma, relational). The source of truth for all structured, relational data: users, teachers, students, courses, enrollment, curriculum (subjects → units → topics), exercises, attempts, mastery state (StudentTopicMastery with pKnown and trend7d), teacher alerts, guides, and the ErrorTag catalog. Managed via Prisma 7 migrations; accessed through @prisma/adapter-pg with a pg connection pool configured for serverless (connection_limit=1). MongoDB Atlas (raw telemetry). Schema-less, high-volume storage for attempt events (keystrokes, intermediate steps, replay data) and AI job audit records (raw LLM request/response pairs, cost tracking per operation). Written exclusively by the telemetryWorker Lambda; read by analytics and debugging tooling. Uses Mongoose via @nestjs/mongoose. AWS S3. Object storage for three categories of binary assets: teacher worksheet PDFs (S3_GUIDES_BUCKET), student submission photos (S3_SUBMISSIONS_BUCKET), and handwritten OCR upload images ({service}-{stage}-ocr-uploads — managed bucket, no env var override). Submission photos use random UUID filenames and are purged by an S3 lifecycle policy (30-day expiry). Presigned PUT URLs (TTL from GUIDES_PRESIGNED_PUT_TTL, default 600 s) are issued by POST /guides so clients upload directly to S3 without proxying through Lambda.

innova-ai-engine integration

Heavy AI work is intentionally isolated in a separate Python Lambda service (innova-ai-engine) to avoid inflating the Node.js bundle, manage Python dependency conflicts, and allow independent scaling and cost control. The AI engine handles:
  • PDF extraction — parsing uploaded teacher worksheets into structured question objects
  • OCR — transcribing handwritten student math photos via Gemini Vision
  • LLM classification — classifying UNCLASSIFIED attempts via Claude against the 2,600+ error taxonomy
  • BKT/IRT nightly calibration — recalculating per-topic BKT parameters and per-student IRT ability estimates
  • Exercise generation — producing new exercises for a given topic and difficulty level
This backend owns every SQS queue and S3 bucket. The AI engine is a consumer — it subscribes to the queues this backend creates. This ownership model means the deploy order is always: backend → ai-engine → clients. Deploying the AI engine before the backend would leave it with no queues to subscribe to. Communication is fully asynchronous. No Lambda-to-Lambda synchronous calls exist. SQS messages carry only opaque IDs (e.g. attemptId, guideId); no PII reaches the AI providers.

Deploy topology

PropertyValue
FrameworkServerless Framework 3
Bundlerserverless-esbuild
Runtimenodejs20.x
Regionus-east-1
AWS account751871643325
Custom domainapi.superprofes.app (via serverless-domain-manager)
CI/CDGitHub Actions — ci.yml (type-check + lint + tests on PRs), deploy.yml (deploy on merge to main)
Manual deploy (CI runs this automatically on merge to main):
pnpm build
pnpm prisma migrate deploy
npx serverless deploy --stage prod
Production DATABASE_URL must use the Supabase transaction pooler on port :6543 with the query-string parameters ?pgbouncer=true&connection_limit=1. Without these parameters, Prisma will open a new connection per Lambda invocation and exhaust the Postgres connection limit under any meaningful load. The local .env.example uses a direct local connection on :5433 — remember to swap to the pooler URL before deploying. Note: innova-ai-engine must use the session pooler (:5432) instead, because asyncpg is incompatible with the transaction pooler’s prepared-statement handling.

Explore further

Lambda Functions

Detailed configuration, memory settings, timeout values, and dead-letter queue setup for each of the six Lambda functions.

SQS Queues

Queue names, visibility timeouts, batch sizes, retry policies, and DLQ configuration for every SQS queue owned by this backend.

Data Model

Full Prisma v9 schema walkthrough — entities, enums, relations, and the cost-accounting and privacy design decisions.

Deployment Guide

Step-by-step production deployment runbook, GitHub Actions secrets reference, and the back-merge release flow.

Build docs developers (and LLMs) love