NestJS + Mastra service that powers curriculum generation and the 8-agent learning loop for Cycy.
Hackathon overview, architecture diagrams, and demo script: see the root README.
| Domain | Responsibility |
|---|---|
| Auth | Clerk JWT verification, Profile / Member lookup (raw SQL on cycy tables) |
| Bootstrap | Ingest PDF/DOCX → LLM curriculum plan → persist modules, concepts, quizzes |
| Learning loop | Mastra learningLoopWorkflow — study through mock interview |
| Content bank | Concept, StudyUnit, MCQ, practice, misconceptions |
| Mastery | LearnerConceptState, PracticeAttempt, CourseScore |
| Session | WorkflowSession per conversation |
| Webhook | Push agent messages → cycy POST /api/internal/agent-response |
End-to-end view of the NestJS backend: HTTP entrypoints, domain services, Mastra workflows, all 8 agents, Prisma tools, shared Postgres, LLM, and webhook delivery back to cycy.
flowchart TB
subgraph Cycy["cycy frontend"]
User([Learner])
UI[Next.js UI + Socket.io]
end
subgraph Nest["NestJS :4000 /api/v1"]
direction TB
subgraph Entry["HTTP controllers"]
CurCtrl["CurriculumController<br/>bootstrap · curriculum · content"]
ConvCtrl["ConversationsController<br/>POST /process · GET /session"]
AssCtrl["AssessmentsController<br/>submit · explain-back"]
Other["Progress · Courses · Health"]
end
Auth[ClerkAuthGuard + ProfileService]
Queue[WorkflowQueueService<br/>async in-process jobs]
subgraph BootstrapPipe["Curriculum bootstrap pipeline"]
CBS[CurriculumBootstrapService]
Ingest[MaterialIngestionService<br/>PDF/DOCX from UploadThing]
Plan[CurriculumPlanningService]
Persist[CurriculumPersistenceService]
Shell[CurriculumShellService<br/>cycy Curriculum/Module/Quiz SQL]
end
ConvSvc[ConversationsService]
MastraSvc[MastraService<br/>workflow start / resume]
subgraph LearningSvc["Learning domain"]
Concepts[ConceptService]
Mastery[MasteryService]
Sessions[WorkflowSessionService]
end
Prisma[PrismaService<br/>Neon pooler + reconnect]
WH[CycyWebhookClient]
LLMsvc[LlmService]
end
subgraph Mastra["Mastra instance — mastra.instance.ts"]
direction TB
subgraph Workflows["Registered workflows"]
LLW["learningLoopWorkflow<br/>route-input → branch → orchestrate-loop"]
CBW["curriculumBootstrapWorkflow<br/>(registered for Mastra Studio)"]
end
subgraph Agents["8 Mastra agents"]
direction LR
AG1[curriculumAgent]
AG2[tutorAgent]
AG3[assessmentAgent]
AG4[progressAgent]
AG5[reflectionAgent]
AG6[motivationAgent]
AG7[certificationAgent]
AG8[interviewAgent]
end
subgraph Tools["Prisma-backed tools (createTool)"]
T1[fetchStudyUnit · fetchComprehensionQuestion · fetchPracticeProblem]
T2[logPracticeAttempt · logMisconception · updateCourseScore]
T3[getNextConcept · getModuleConcepts · updateModuleProgress]
T4[updateWorkflowSession · getWorkflowSession · searchKnowledge]
end
Store[MockStore]
end
subgraph External["External"]
OpenAI[OpenAI / OpenRouter]
DB[(Neon PostgreSQL<br/>cycy tables + backend tables)]
WebhookRoute["cycy POST /api/internal/agent-response"]
end
User --> UI
UI -->|"Bearer JWT"| Entry
Entry --> Auth
CurCtrl --> CBS
CBS --> Ingest --> Plan --> Persist --> Shell
Plan --> AG1
Plan --> LLMsvc
ConvCtrl --> ConvSvc --> Queue --> MastraSvc
AssCtrl --> MastraSvc
MastraSvc --> LLW
LLW --> Orchestrate["orchestrate-loop.logic<br/>+ tutor-step · assessment-step · …"]
Orchestrate --> Agents
Agents --> Tools
Tools --> LearningSvc
LearningSvc --> Prisma
Shell --> Prisma
Ingest --> Prisma
Persist --> Prisma
Prisma --> DB
Agents --> OpenAI
LLMsvc --> OpenAI
MastraSvc --> WH
AssCtrl --> WH
WH -->|"X-Internal-Secret"| WebhookRoute --> UI --> User
Triggered by POST /servers/:serverId/bootstrap. Runs async (202) unless BOOTSTRAP_SYNC=true.
flowchart LR
A["POST /bootstrap"] --> B[CurriculumBootstrapService]
B --> C{Admin + status PENDING/FAILED?}
C -->|no| X[409 Conflict]
C -->|yes| D[set Curriculum GENERATING]
D --> E[MaterialIngestionService<br/>fetch PDF/DOCX · extract text]
E --> F[CurriculumPlanningService]
F --> G["curriculumAgent.generate()<br/>+ LlmService structured JSON fallback"]
G --> H[CurriculumPersistenceService<br/>Concept · StudyUnit · MCQ · Quiz · Meta]
H --> I[CurriculumShellService<br/>Module · QuizQuestion · FinalExam]
I --> J[set Curriculum READY]
J --> K["GET /curriculum → READY<br/>GET /curriculum/content"]
style G fill:#6366f1,color:#fff
Tables written: cycy-owned Curriculum, Module, Quiz, QuizQuestion, FinalExam + backend-owned Concept, StudyUnit, ComprehensionQuestion, PracticeProblem, Misconception, ServerLearningMeta.
POST /conversations/:id/process and POST /assessments/* both call MastraService.process(), which starts or resumes learningLoopWorkflow using WorkflowSession state.
flowchart TB
Start["POST /process or /assessments/submit"] --> MS[MastraService.process]
MS --> Bind[bindLearningLoopDeps per conversationId]
Bind --> Session[WorkflowSession getOrCreate]
Session --> Resume{Suspended run<br/>+ step expects input?}
Resume -->|yes| R[workflow.run.resume]
Resume -->|no| S[workflow.run.start]
R --> LLW[learningLoopWorkflow]
S --> LLW
LLW --> Route["step: route-input<br/>free_chat vs structured"]
Route -->|free_chat| FC["step: free-chat<br/>handleFreeChat → tutorAgent"]
Route -->|structured| OL["step: orchestrate-loop<br/>runStructuredLoop"]
OL --> StepSwitch{WorkflowSession.step}
StepSwitch -->|STUDY| T1["tutorAgent<br/>study unit explanation"]
StepSwitch -->|QUICK_CHECK| A1["assessmentAgent<br/>grade MCQ"]
StepSwitch -->|PRACTICE| A2["assessmentAgent<br/>grade practice"]
StepSwitch -->|EXPLAIN_BACK| A3["assessmentAgent<br/>explain-back rubric"]
StepSwitch -->|MICRO_DRILL| A4["assessmentAgent<br/>misconception drill"]
StepSwitch -->|CONCEPT_COMPLETE| P1["progressAgent + reflectionAgent<br/>+ motivationAgent"]
StepSwitch -->|MODULE_GATE| A5["assessmentAgent<br/>gate quiz + progressAgent unlock"]
StepSwitch -->|GOAL_VERIFICATION| GV["runGoalVerification<br/>LLM + ServerLearningMeta"]
StepSwitch -->|MOCK_INTERVIEW| I1["interviewAgent<br/>mock Q&A"]
StepSwitch -->|INTERVIEW_DEBRIEF| I2["interviewAgent<br/>debrief + retry"]
StepSwitch -->|COURSE_COMPLETE| C1["certificationAgent<br/>certificate + summary"]
T1 & A1 & A2 & A3 & A4 & P1 & A5 & GV & I1 & I2 & C1 --> Tools[Prisma tools + Concept/Mastery services]
FC --> Tools
Tools --> Suspend{Needs user input?}
Suspend -->|yes| Sus["workflow.suspend<br/>store mastraRunId in session"]
Suspend -->|no| Done[AgentMessagePayload array]
Suspend --> WH
Done --> WH[CycyWebhookClient → cycy]
WH --> Unbind[unbindLearningLoopDeps]
style LLW fill:#6366f1,color:#fff
style OL fill:#6366f1,color:#fff
| Agent | Used in | Purpose |
|---|---|---|
| curriculumAgent | Bootstrap | Generate full curriculum plan JSON from ingested materials |
| tutorAgent | STUDY, FREE_CHAT | Explain study units; answer off-path questions (no tools in free chat) |
| assessmentAgent | QUICK_CHECK, PRACTICE, EXPLAIN_BACK, MICRO_DRILL, MODULE_GATE | Grade MCQ, practice, explain-back, drills, module gate quizzes |
| progressAgent | CONCEPT_COMPLETE, MODULE_GATE | Advance concepts, unlock modules, update mastery |
| reflectionAgent | CONCEPT_COMPLETE | Post-concept reflection prompt |
| motivationAgent | After assessments / concept complete | Encouragement, XP milestones |
| certificationAgent | GOAL_VERIFICATION, COURSE_COMPLETE | Provisional/final certificates, goal summary |
| interviewAgent | MOCK_INTERVIEW, INTERVIEW_DEBRIEF | Job-prep mock interview Q&A and debrief |
All agents (except curriculum bootstrap path) receive Prisma tools from createLearningTools() in src/ai/tools/.
flowchart LR
subgraph Bootstrap["App bootstrap"]
AppMod[AppModule] --> AiMod[AiModule]
AiMod --> OnInit[MastraService.onModuleInit]
OnInit --> Create[createMastraInstance]
end
Create --> Reg["Mastra({ agents, workflows, storage })"]
Reg --> Agents8[8 × createXAgent]
Reg --> WF2[learningLoopWorkflow + curriculumBootstrapWorkflow]
Create --> Deps[LearningLoopDeps registry<br/>bind per conversationId]
subgraph Runtime["Per request"]
Process[MastraService.process] --> Ctx[RuntimeContext<br/>conversationId only]
Process --> WFRun[workflow.createRunAsync]
WFRun --> Steps["route-input · free-chat · orchestrate-loop"]
Steps --> Logic["*-step.logic.ts files"]
Logic --> DepsLookup[getDeps via conversationId]
end
style Reg fill:#6366f1,color:#fff
Deep dive (security, deferred items): docs/ARCHITECTURE.md
cd backend
pnpm install
cp .env.example .env
# Set DATABASE_URL, CLERK_SECRET_KEY, CYCY_INTERNAL_SECRET, OPENAI_API_KEY, CYCY_URL# 1. cycy migrations first
cd ../cycy && pnpm prisma migrate deploy
# 2. backend migrations
cd ../backend && pnpm prisma migrate deployTable ownership and migration order: docs/CONCURRENT_DB.md.
Production: use Neon pooler URL (…-pooler.….neon.tech). PrismaService automatically appends pgbouncer=true&connect_timeout=10&pool_timeout=10 and retries transient connection errors (P1017).
Product path is bootstrap from onboarding, not seed. For local workflow testing:
- Create a server in cycy UI
- Set
SERVER_IDin.env - Run
pnpm prisma db seed
pnpm dev # http://localhost:4000
curl localhost:4000/api/v1/healthSwagger: http://localhost:4000/api/docs — Authorize with Bearer <Clerk JWT>.
| Method | Path | Description |
|---|---|---|
| GET | /health |
Liveness + DB ping |
| POST | /servers/:serverId/bootstrap |
Generate curriculum (202 async; sync with BOOTSTRAP_SYNC=true) |
| GET | /servers/:serverId/curriculum |
Status + module summary (poll until READY) |
| GET | /servers/:serverId/curriculum/content |
Full generated content |
| GET | /courses/:serverId/concepts |
Content bank for sidebar |
| GET | /progress/:serverId |
Mastery + XP |
| GET | /conversations/:id/session?serverId= |
Workflow step state |
| POST | /conversations/:id/process |
Trigger / resume learning loop (202 async) |
| POST | /assessments/submit |
MCQ / gate quiz answer |
| POST | /assessments/explain-back |
Explain-back grading |
Payloads: docs/API.md
Wiring is implemented end-to-end:
cycy onboarding → POST /bootstrap
user DM message → POST /process (LearningSession.id = conversationId)
backend workflow → POST cycy/api/internal/agent-response (X-Internal-Secret)
cycy → persist Message + Socket.io emit
Guide: docs/FRONTEND_INTEGRATION.md
src/
├── auth/ Clerk guard + ProfileService (raw SQL on cycy tables)
├── curriculum/ Bootstrap pipeline, shell queries, content API
├── materials/ PDF/DOCX ingestion from UploadThing URLs
├── ai/
│ ├── agents/ 8 Mastra agents
│ ├── tools/ Prisma-backed agent tools
│ ├── workflows/ learningLoopWorkflow, curriculum plan step
│ └── mastra.service.ts
├── learning/ Concept, Mastery, WorkflowSession
├── llm/ OpenAI / OpenRouter provider
├── conversations/ POST /process
├── assessments/ Grading endpoints
├── progress/ Mastery API
├── webhooks/ Outbound client → cycy
└── prisma/ PrismaService + Neon pooler URL helper
SERVER_ID=clx... pnpm test:bootstrap # curriculum from real server
SERVER_ID=... PROFILE_ID=... pnpm test:workflow # loop without HTTP
pnpm test # Jest unit tests- Root directory:
backend - Health check:
/api/v1/health - Startup:
docker-entrypoint.shrunsprisma migrate deploythennode dist/main.js
Required env: DATABASE_URL, CLERK_SECRET_KEY, CYCY_INTERNAL_SECRET, OPENAI_API_KEY, CYCY_URL, FRONTEND_URL, NODE_ENV=production
| File | Purpose |
|---|---|
| docs/ARCHITECTURE.md | System design, agents, security |
| docs/API.md | REST contracts |
| docs/FRONTEND_INTEGRATION.md | Webhook + chat integration |
| docs/CONCURRENT_DB.md | Shared DB rules |
| docs/SCHEMA.md | Backend Prisma models |
| ROADMAP.md | Phased build plan |
PORT=4000
DATABASE_URL=postgresql://...@ep-xxx-pooler....neon.tech/...?sslmode=require
CLERK_SECRET_KEY=sk_test_...
OPENAI_API_KEY=sk-or-v1-... # OpenRouter supported
CYCY_URL=https://your-cycy-app.vercel.app
CYCY_INTERNAL_SECRET=long-random-string
FRONTEND_URL=https://your-cycy-app.vercel.app
BOOTSTRAP_SYNC=true # optional — sync bootstrap for demos
SERVER_ID=clx... # seed / test scripts only