Personalized, AI-driven learning paths — built in support of UN Sustainable Development Goal 4: Quality Education.
Access to good mentorship and a clear learning path is still a privilege, not a default. SkillPath AI addresses that gap: it generates a personalized learning roadmap based on a user's goal, current skill level, and available time, then supports them through it with an AI chat assistant for doubts, curated resources, and project recommendations matched to their progress.
Most self-learners face the same three obstacles:
- Decision fatigue — too many scattered tutorials, no clear sequence to follow.
- No one to ask — questions come up at odd hours with no mentor available.
- Theory without practice — courses teach concepts but rarely suggest what to actually build.
SkillPath AI targets each of these directly with a roadmap generator, a 24/7 AI assistant, and project recommendations calibrated to skill level.
- Features
- Tech Stack
- Architecture Overview
- Project Structure
- Authentication and Security Model
- Data Model
- The AI Layer and Fallback Strategy
- API Reference
- Master App Flow
- Feature Flowcharts
- Registration
- Login
- Session restore on app load
- Route protection
- Onboarding
- Roadmap generation
- Marking a roadmap step complete
- Generating a step lesson
- Generating a step quiz
- AI chat mentor
- Project recommendations
- Saving a project
- Viewing resources
- Admin adds a resource
- Admin deletes a resource
- Profile update
- Admin platform stats
- Admin deletes a user
- Logout
- Environment Variables
- Local Development
- Deployment
- Known Limitations and Future Work
- Account system. Register and log in with email and password, JWT based sessions stored in an HttpOnly cookie.
- Onboarding. New users set a learning goal, skill level, and weekly time commitment before reaching the dashboard.
- AI generated roadmaps. A step by step learning path generated from the user's goal, level, and available hours, produced by an LLM and stored per user.
- Progress tracking. Each roadmap step can be marked complete independently, persisted per user per roadmap.
- AI chat mentor. A persistent chat conversation with an AI tutor, with full history stored and replayed on return visits.
- AI generated lessons and quizzes. Per roadmap step, the app can generate a short markdown lesson and a multiple choice quiz on demand.
- Project recommendations. AI suggested project ideas matched to the user's level and goal, with the ability to save favorites.
- Curated resources. A shared resource library, readable by any logged in user, writable only by admins.
- Admin panel. Platform wide stats, user management, and resource management, gated behind an admin only role check on both frontend and backend.
- Graceful AI degradation. If no AI API key is configured, or the AI call fails, the app falls back to deterministic, hand written roadmap and project data instead of breaking.
| Layer | Technology |
|---|---|
| Backend framework | Node.js, Express 5 |
| Backend auth | JSON Web Tokens in an HttpOnly cookie |
| Database | MongoDB via Mongoose |
| AI provider | Groq (llama-3.3-70b-versatile) |
| Security middleware | Helmet, CORS, express-rate-limit |
| Frontend framework | React 19 |
| Routing | React Router v6 |
| HTTP client | Axios, with withCredentials: true |
| State management | React Context (AuthContext, ThemeContext) |
| Build tool | Vite |
| Backend hosting | Render |
| Frontend hosting | Vercel |
Two independently deployed services, frontend and backend, on different domains, communicating over HTTPS with a JWT carried in a cross site cookie. The backend never renders HTML; it is a pure JSON API. The AI provider is a third external dependency the backend talks to server side, so the AI API key is never exposed to the browser.
flowchart LR
subgraph Browser
UI[React SPA<br/>Vite build, React Router]
end
subgraph Vercel
Static[Static hosting]
end
subgraph Render
API[Express API<br/>JWT auth, rate limiting]
DB[(MongoDB)]
end
subgraph Groq
LLM[llama-3.3-70b-versatile]
end
UI -- "loads bundle" --> Static
UI -- "axios, withCredentials: true" --> API
API -- "mongoose queries" --> DB
API -- "chat completions" --> LLM
LLM -. "on failure or missing key" .-> API
API -- "Set-Cookie: token (HttpOnly, JWT)" --> UI
Key architectural decisions:
- JWT in an HttpOnly cookie, not
localStorage. The frontend never touches the token directly. It cannot be read or exfiltrated by injected JavaScript, sincehttpOnly: truemakes it invisible todocument.cookie. The browser just attaches it automatically on every request because Axios is configured withwithCredentials: true. - No CSRF token dance. Unlike a typical session cookie setup, this backend does not implement CSRF tokens at all. The tradeoff is deliberate: it relies entirely on
SameSitecookie policy and a strict CORS origin allowlist to block cross site request forgery, rather than a second token mechanism. This is simpler to reason about but means the CORS configuration is the actual security boundary and must be airtight. - AI calls happen only on the backend. The Groq API key lives in a server side environment variable. The frontend never calls Groq directly; it always goes through the Express API, which then decides whether to call the LLM or fall back to static logic.
- Fallback over failure. Every AI dependent controller degrades to a deterministic function (
fallbackService.js) rather than returning an error, whether the cause is a missing API key, a network failure, or a malformed AI response that fails JSON parsing. - Rate limiting at two tiers. A stricter limiter on
/api/auth/*(20 requests per 15 minutes) resists credential stuffing and brute force login attempts, and a looser general limiter on/apioverall (100 requests per 15 minutes) protects the rest of the API and, incidentally, the AI provider's usage quota.
SkillPathAI/
├── backend/
│ ├── server.js # app entrypoint, middleware chain, route mounting
│ ├── config/
│ │ ├── db.js # mongoose connection
│ │ └── env.js # startup env validation, CORS origin parsing
│ ├── models/
│ │ ├── User.js # email, hashed password, isAdmin, onboarded
│ │ ├── Profile.js # goal, level, weeklyHours, bio
│ │ ├── Roadmap.js # AI-generated steps, one active per user
│ │ ├── Progress.js # completedSteps per user per roadmap
│ │ ├── ChatHistory.js # one growing message thread per user
│ │ ├── SavedProject.js # user's bookmarked project ideas
│ │ └── Resource.js # admin-curated learning resources
│ ├── controllers/ # one file per resource, thin route handlers
│ ├── routes/ # one file per resource, wires controllers to middleware
│ ├── middleware/
│ │ ├── authMiddleware.js # protect (JWT check), admin (role check)
│ │ ├── errorMiddleware.js # notFound + centralized error handler
│ │ └── rateLimiter.js # authLimiter, apiLimiter
│ ├── services/
│ │ ├── aiService.js # all Groq calls, with try/catch fallback
│ │ └── fallbackService.js # deterministic roadmap/project/chat data
│ └── utils/
│ └── generateToken.js # signs JWT, sets the cookie
└── frontend/
└── src/
├── main.jsx
├── App.jsx # route table
├── context/
│ ├── AuthContext.jsx # user state, login/register/logout, refreshUser
│ └── ThemeContext.jsx
├── routes/
│ ├── ProtectedRoute.jsx # redirects to /login if not authenticated
│ └── AdminRoute.jsx # redirects to /dashboard if not admin
├── utils/
│ └── api.js # axios instance, withCredentials: true
├── pages/ # one file per route: Login, Register, Dashboard,
│ # Roadmap, Chat, Projects, Resources, Profile, Onboarding,
│ # and an admin/ subfolder for admin-only pages
└── components/
├── landing/
├── layout/ # Navbar, Footer, Sidebar (AppShell)
├── learning/
└── pages/
Registration and login both call generateToken(res, userId), which signs a JWT ({ userId }, 30 day expiry) and sets it as a cookie named token:
res.cookie("token", token, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: process.env.NODE_ENV === "production" ? "None" : "Lax",
maxAge: 30 * 24 * 60 * 60 * 1000,
path: "/",
});httpOnly: true is the important part: JavaScript on the frontend, whether the app's own code or an injected script from an XSS vulnerability, cannot read this cookie. It can only be sent by the browser automatically on requests to the backend's domain, which is why Axios is configured with withCredentials: true on the frontend side.
In production, sameSite: "None" combined with secure: true is required for the cookie to be sent on the cross origin requests this architecture depends on, since frontend and backend are on different domains.
admin middleware runs after protect and simply checks req.user.isAdmin. It is applied at the router level for entire resources (router.use(protect, admin) in userRoutes.js and adminRoutes.js) and per route for the resource routes that mix public reads with admin only writes (resourceRoutes.js: anyone logged in can GET, only admins can POST or DELETE).
cors({
origin(origin, callback) {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, origin || allowedOrigins[0]);
} else {
callback(new Error(`CORS blocked for origin: ${origin}`));
}
},
credentials: true,
})allowedOrigins comes from the CLIENT_URL environment variable. Because this app does not use CSRF tokens, this allowlist is doing real security work, not just convenience: it is what stops an arbitrary third party site from making authenticated, cookie carrying requests against the API on a logged in user's behalf.
erDiagram
USER ||--o| PROFILE : has
USER ||--o{ ROADMAP : owns
USER ||--o{ PROGRESS : tracks
USER ||--o| CHATHISTORY : has
USER ||--o{ SAVEDPROJECT : saves
USER ||--o{ RESOURCE : "adds (if admin)"
ROADMAP ||--o{ PROGRESS : "tracked by"
USER {
ObjectId _id
string name
string email
string password_hash
bool isAdmin
bool onboarded
}
PROFILE {
ObjectId user FK
string goal
string level
number weeklyHours
string bio
}
ROADMAP {
ObjectId user FK
string goal
string level
string estimatedDuration
bool aiGenerated
array steps
}
PROGRESS {
ObjectId user FK
ObjectId roadmap FK
array completedSteps
}
CHATHISTORY {
ObjectId user FK
array messages
}
SAVEDPROJECT {
ObjectId user FK
string title
string difficulty
array techStack
}
RESOURCE {
string title
string category
ObjectId addedBy FK
}
Notable design choices in the schema:
- One active roadmap per user.
generateRoadmapdeletes the user's existingRoadmapandProgressdocuments before creating new ones. There is no history of past roadmaps; generating a new one replaces the old. Progressis decoupled fromRoadmapsteps.completedStepsis just an array of step numbers, not a reference into the roadmap'sstepsarray. The frontend is responsible for matching step numbers back to step content when rendering.ChatHistoryis a single growing document per user, not one document per message. Every chat turn appends to the samemessagesarray, capped only by MongoDB's document size limit in practice.
services/aiService.js is the only place in the codebase that talks to Groq. Every exported function follows the same shape:
flowchart TD
Start([Controller calls an aiService function]) --> Check{GROQ_API_KEY set?}
Check -- No --> Fallback[Return static fallback data]
Check -- Yes --> Call[Call Groq chat.completions.create]
Call --> Parse{Response parses as expected JSON or text?}
Parse -- Yes --> Return[Return AI-generated result]
Parse -- No / API error --> Fallback
Fallback --> Return
This matters for two reasons worth mentioning in an interview. First, it means the app is fully demoable and testable without ever configuring an AI key, since fallbackService.js provides hand written, deterministic roadmap and project data. Second, it means a flaky or rate limited AI provider never surfaces as a 500 error to the user, a try/catch around every Groq call always resolves to something usable.
The five AI backed capabilities:
| Function | Purpose | Fallback behavior |
|---|---|---|
generateRoadmapWithAI |
Builds the step by step learning path | Static roadmap template |
generateProjectRecommendations |
Suggests 3 to 5 project ideas | Static project list |
chatWithAI |
Powers the chat mentor, full conversation history sent as context | A single canned offline message |
generateStepLesson |
Mini markdown lesson for one roadmap step | Instructional text pointing to official docs |
generateStepQuiz |
3 to 5 question multiple choice quiz for one step | Returns null, controller responds 503 |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /api/auth/register |
No | Create account, sets JWT cookie |
| POST | /api/auth/login |
No | Log in, sets JWT cookie |
| POST | /api/auth/logout |
No | Clears the JWT cookie |
| GET | /api/auth/me |
Yes | Current user |
| GET | /api/profiles/me |
Yes | Get own profile |
| PUT | /api/profiles/me |
Yes | Update profile, flips onboarded to true once goal/level/hours are set |
| POST | /api/roadmaps/generate |
Yes | Generate (and replace) the user's roadmap |
| GET | /api/roadmaps/me |
Yes | Get current roadmap |
| GET | /api/progress/me |
Yes | Get progress on current roadmap |
| PUT | /api/progress/me |
Yes | Update completed steps |
| POST | /api/chat |
Yes | Send a chat message, get an AI reply |
| GET | /api/chat/history |
Yes | Full chat history |
| POST | /api/learning/lesson |
Yes | Generate a lesson for a step |
| POST | /api/learning/quiz |
Yes | Generate a quiz for a step |
| GET | /api/projects/suggestions |
Yes | AI project recommendations |
| POST | /api/projects/save |
Yes | Bookmark a project |
| GET | /api/projects/saved |
Yes | List bookmarked projects |
| GET | /api/resources |
Yes | List all resources |
| POST | /api/resources |
Yes, admin | Add a resource |
| DELETE | /api/resources/:id |
Yes, admin | Delete a resource |
| GET | /api/users |
Yes, admin | List all users |
| DELETE | /api/users/:id |
Yes, admin | Delete a user (not self) |
| GET | /api/admin/stats |
Yes, admin | Platform wide counts |
| GET | /api/admin/users |
Yes, admin | List users (admin view) |
| GET | /health |
No | Liveness and DB connectivity check |
This is the entire application, start to finish, as one diagram: every screen a user can land on and what pushes them from one to the next.
flowchart TD
Visit([User visits the site]) --> SessionCheck{GET /api/auth/me<br/>valid session cookie?}
SessionCheck -- No --> Landing[Landing Page]
Landing --> ChooseAuth{Register or Login?}
ChooseAuth -- Register --> RegisterPage
ChooseAuth -- Login --> LoginPage
RegisterPage -- success --> Onboarded{onboarded flag?}
LoginPage -- success --> Onboarded
SessionCheck -- Yes --> Onboarded
Onboarded -- false --> OnboardingPage[Onboarding: set goal, level, weeklyHours]
OnboardingPage -- profile saved, onboarded = true --> RoadmapGen[Auto-trigger roadmap generation]
RoadmapGen --> Dashboard
Onboarded -- true --> Dashboard[Dashboard]
Dashboard --> RoadmapPage[Roadmap Page:<br/>view steps, mark complete,<br/>generate lesson / quiz per step]
Dashboard --> ChatPage[Chat Page:<br/>AI mentor conversation]
Dashboard --> ProjectsPage[Projects Page:<br/>AI suggestions, save favorites]
Dashboard --> ResourcesPage[Resources Page:<br/>browse curated links]
Dashboard --> ProfilePage[Profile Page:<br/>edit goal, level, hours, bio]
RoadmapPage -- "regenerate roadmap" --> RoadmapGen
Dashboard -- "isAdmin only" --> AdminDashboard[Admin Dashboard: platform stats]
AdminDashboard --> AdminUsers[Admin: manage users]
AdminDashboard --> AdminResources[Admin: manage resources]
Dashboard --> Logout([Logout])
RoadmapPage --> Logout
ChatPage --> Logout
ProjectsPage --> Logout
ResourcesPage --> Logout
ProfilePage --> Logout
Logout --> Landing
The single fork that shapes the entire user experience is the onboarded boolean on the User document. Every authenticated entry point funnels through that check: incomplete profile always lands on Onboarding, regardless of which URL the user actually typed in or bookmarked, because ProtectedRoute guards the destination but does not itself redirect based on onboarded, individual pages and the post login flow handle that.
Each diagram below traces one user facing action from the click to the database and back.
flowchart TD
A([User fills name, email, password]) --> B[POST /api/auth/register]
B --> C{authLimiter: under 20 req/15min?}
C -- No --> D[429 Too many auth attempts]
C -- Yes --> E{Name, email, password all present?<br/>Password >= 6 chars?}
E -- No --> F[400 validation error]
E -- Yes --> G{Email already registered?}
G -- Yes --> H[400 User already exists]
G -- No --> I[User.create<br/>pre-save hook bcrypt-hashes password]
I --> J[generateToken: sign JWT, set HttpOnly cookie]
J --> K[201 Created, returns user object]
K --> L[AuthContext.setUser]
L --> M[Redirect to /onboarding]
flowchart TD
A([User submits email + password]) --> B[POST /api/auth/login]
B --> C{authLimiter: under 20 req/15min?}
C -- No --> D[429 Too many auth attempts]
C -- Yes --> E{User with this email exists?}
E -- No --> F[401 Invalid email or password]
E -- Yes --> G[bcrypt.compare submitted password vs stored hash]
G -- No match --> F
G -- Match --> H[generateToken: sign JWT, set HttpOnly cookie]
H --> I[200 OK, returns user object]
I --> J[AuthContext.setUser]
J --> K{user.onboarded?}
K -- false --> L[Redirect to /onboarding]
K -- true --> M[Redirect to /dashboard]
flowchart TD
A([App mounts]) --> B[AuthProvider useEffect fires refreshUser]
B --> C[GET /api/auth/me<br/>browser auto-attaches token cookie]
C --> D{Cookie present and valid JWT?}
D -- No cookie --> E[401 Not authorized, no token]
D -- Invalid / expired --> F[401 Not authorized, token invalid]
D -- Valid --> G[User.findById from decoded payload]
G -- Not found --> H[401 Not authorized, user not found]
G -- Found --> I[200, returns user object]
E & F & H --> J[AuthContext: user = null]
I --> K[AuthContext: user = returned user]
J & K --> L[loading = false]
L --> M[Route guards now evaluate correctly]
flowchart TD
A([User navigates to a route]) --> B{Route type?}
B -- Public --> C[Render directly:<br/>Landing, About, Contact, Login, Register]
B -- Protected --> D{AuthContext.loading?}
D -- true --> E[Show loading state]
D -- false --> F{user exists?}
F -- No --> G[Navigate to /login, remember attempted path]
F -- Yes --> H[Render the page inside AppShell]
B -- Admin-only --> I{AuthContext.loading?}
I -- true --> E
I -- false --> J{user?.isAdmin?}
J -- No --> K[Navigate to /dashboard]
J -- Yes --> L[Delegate to ProtectedRoute check above]
L --> H
flowchart TD
A([New user lands on Onboarding page]) --> B[Fill goal, level, weeklyHours, optional bio]
B --> C[PUT /api/profiles/me]
C --> D{Profile document exists for user?}
D -- No --> E[new Profile]
D -- Yes --> F[load existing Profile]
E & F --> G[apply only the fields sent in the request body]
G --> H[profile.save]
H --> I{goal AND level set AND weeklyHours > 0?}
I -- Yes, and user.onboarded was false --> J[User.findByIdAndUpdate: onboarded = true]
I -- No --> K[leave onboarded as-is]
J & K --> L[200, returns updated profile]
L --> M[Frontend triggers roadmap generation]
M --> N[Redirect to /dashboard]
flowchart TD
A([User clicks Generate Roadmap]) --> B[POST /api/roadmaps/generate]
B --> C[Load user's Profile as fallback for goal/level/weeklyHours]
C --> D{goal and level resolved from body or profile?}
D -- No --> E[400 Goal and level are required]
D -- Yes --> F[generateRoadmapWithAI: goal, level, weeklyHours]
F --> G{GROQ_API_KEY set?}
G -- No --> H[generateFallbackRoadmap: static template]
G -- Yes --> I[Call Groq with strict JSON-only system prompt]
I --> J{Response parses as valid JSON?}
J -- No --> H
J -- Yes --> K[Use AI-generated steps + estimatedDuration]
H --> L[Delete user's old Roadmap and Progress documents]
K --> L
L --> M[Roadmap.create with new steps]
M --> N[Progress.findOneAndUpdate upsert: fresh, empty completedSteps]
N --> O[201, returns new roadmap]
O --> P[Frontend renders roadmap steps]
flowchart TD
A([User toggles a step's checkbox]) --> B[PUT /api/progress/me<br/>stepNumber, completed: true/false]
B --> C[getOrCreateProgress: find latest Roadmap for user]
C --> D{Roadmap exists?}
D -- No --> E[404 No roadmap to track progress]
D -- Yes --> F{Progress doc exists for this roadmap?}
F -- No --> G[Progress.create with empty completedSteps]
F -- Yes --> H[load existing Progress]
G & H --> I{Request sent completedSteps array directly?}
I -- Yes --> J[Replace completedSteps wholesale]
I -- No, sent stepNumber + completed --> K{completed === false?}
K -- Yes --> L[Remove stepNumber from the Set]
K -- No --> M[Add stepNumber to the Set]
L & M --> N[Sort and dedupe into completedSteps array]
J & N --> O[progress.save]
O --> P[200, returns updated progress]
P --> Q[Frontend updates checklist / progress bar]
flowchart TD
A([User clicks 'Learn' on a roadmap step]) --> B[POST /api/learning/lesson<br/>stepTitle, optional goal/level]
B --> C{stepTitle provided?}
C -- No --> D[400 stepTitle is required]
C -- Yes --> E[Load Profile to fill in missing goal/level]
E --> F[generateStepLesson]
F --> G{GROQ_API_KEY set?}
G -- No --> H[Static markdown pointing to official docs]
G -- Yes --> I[Call Groq: concise markdown mini-lesson]
I --> J{Call succeeds?}
J -- No --> H
J -- Yes --> K[Return AI-written lesson]
H & K --> L["200, { lesson }"]
L --> M[Frontend renders markdown in a panel]
flowchart TD
A([User clicks 'Quiz me' on a roadmap step]) --> B[POST /api/learning/quiz<br/>stepTitle, optional goal]
B --> C{stepTitle provided?}
C -- No --> D[400 stepTitle is required]
C -- Yes --> E[Load Profile to fill in missing goal]
E --> F[generateStepQuiz]
F --> G{GROQ_API_KEY set?}
G -- No --> H[Return null]
G -- Yes --> I[Call Groq: JSON array of 3-5 MCQs]
I --> J{Parses as valid JSON array?}
J -- No --> H
J -- Yes --> K[Return parsed quiz array]
H --> L[503 Quiz generation unavailable]
K --> M["200, { quiz }"]
M --> N[Frontend renders interactive multiple choice quiz]
flowchart TD
A([User opens Chat page]) --> B[GET /api/chat/history]
B --> C[Load or return empty ChatHistory.messages]
C --> D[Render existing conversation]
D --> E([User types a message and sends])
E --> F[POST /api/chat, message text]
F --> G[Find or create ChatHistory for user]
G --> H[Append user message to DB messages array]
H --> I[chatWithAI: system prompt + full message history]
I --> J{GROQ_API_KEY set and call succeeds?}
J -- No --> K[offlineChatMessage fallback text]
J -- Yes --> L[AI-generated reply]
K & L --> M[Append assistant message to DB messages array, save]
M --> N["200, { response }"]
N --> O[Frontend appends both messages to chat UI]
O --> E
flowchart TD
A([User opens Projects page]) --> B[GET /api/projects/suggestions?level&goal]
B --> C[Load Profile as fallback for level/goal if query params absent]
C --> D[generateProjectRecommendations: level, goal]
D --> E{GROQ_API_KEY set?}
E -- No --> F[generateFallbackProjects: static list]
E -- Yes --> G[Call Groq: JSON array of 3-5 project ideas]
G --> H{Parses as valid JSON array?}
H -- No --> F
H -- Yes --> I[Use AI-generated project list]
F & I --> J[200, returns array of projects]
J --> K[Frontend renders project cards]
flowchart TD
A([User clicks Save on a project card]) --> B[POST /api/projects/save<br/>title, description, difficulty, techStack, estimatedTime]
B --> C{title present?}
C -- No --> D[400 Title is required]
C -- Yes --> E[SavedProject.create, tied to req.user._id]
E --> F[201, returns saved project]
F --> G[Frontend marks card as saved / adds to Saved list]
H([User opens Saved Projects view]) --> I[GET /api/projects/saved]
I --> J[SavedProject.find, sorted by savedAt descending]
J --> K[200, returns array]
K --> L[Frontend renders saved project list]
flowchart TD
A([User opens Resources page]) --> B[GET /api/resources]
B --> C{protect: valid session?}
C -- No --> D[401 Not authorized]
C -- Yes --> E[Resource.find, sorted by createdAt descending]
E --> F[200, returns array]
F --> G[Frontend renders resource list, no admin controls shown to non-admins]
flowchart TD
A([Admin fills resource form]) --> B[POST /api/resources<br/>title, description, url, category]
B --> C{protect: valid session?}
C -- No --> D[401 Not authorized]
C -- Yes --> E{admin: req.user.isAdmin?}
E -- No --> F[403 Admin access required]
E -- Yes --> G{title present?}
G -- No --> H[400 Title is required]
G -- Yes --> I[Resource.create, addedBy = req.user._id]
I --> J[201, returns new resource]
J --> K[Frontend prepends it to the resource list]
flowchart TD
A([Admin clicks Delete on a resource]) --> B[DELETE /api/resources/:id]
B --> C{protect + admin checks pass?}
C -- No --> D[401 / 403]
C -- Yes --> E{Resource.findById returns a document?}
E -- No --> F[404 Resource not found]
E -- Yes --> G[resource.deleteOne]
G --> H["200, { message: Resource deleted }"]
H --> I[Frontend removes it from the list]
flowchart TD
A([User edits goal, level, weeklyHours, bio, or avatar on Profile page]) --> B[PUT /api/profiles/me]
B --> C{Profile exists for user?}
C -- No --> D[new Profile instance]
C -- Yes --> E[load existing Profile]
D & E --> F[apply only fields present in the request body]
F --> G[profile.save]
G --> H{goal, level set and weeklyHours > 0, and user.onboarded was false?}
H -- Yes --> I[flip User.onboarded to true]
H -- No --> J[no change to onboarded]
I & J --> K[200, returns updated profile]
K --> L[Frontend reflects saved changes]
flowchart TD
A([Admin opens Admin Dashboard]) --> B[GET /api/admin/stats]
B --> C{protect + admin checks pass?}
C -- No --> D[401 / 403]
C -- Yes --> E[Promise.all: count Users, Roadmaps, Progress docs,<br/>ChatHistory docs, SavedProjects, Resources, onboarded Users]
E --> F[200, returns aggregated counts object]
F --> G[Frontend renders stat cards]
flowchart TD
A([Admin clicks Delete on a user row]) --> B[DELETE /api/users/:id]
B --> C{protect + admin checks pass?}
C -- No --> D[401 / 403]
C -- Yes --> E{User.findById returns a document?}
E -- No --> F[404 User not found]
E -- Yes --> G{target _id equals req.user._id?}
G -- Yes --> H[400 Cannot delete your own account]
G -- No --> I[user.deleteOne]
I --> J["200, { message: User removed }"]
J --> K[Frontend removes them from the admin user list]
Note: deleting a User does not cascade to their Profile, Roadmap, Progress, ChatHistory, or SavedProject documents; those remain in the database, orphaned. Worth naming as a known gap if asked about data integrity.
flowchart TD
A([User clicks Logout]) --> B[POST /api/auth/logout]
B --> C[res.cookie 'token' = '', expires immediately]
C --> D["200, { message: Logged out successfully }"]
D --> E[AuthContext.setUser null]
E --> F[Redirect to Landing page]
| Variable | Required | Purpose |
|---|---|---|
MONGO_URI (or MONGO_URL) |
Yes | MongoDB connection string |
JWT_SECRET |
Yes | Signs and verifies the auth JWT |
CLIENT_URL |
Recommended in production | Comma separated list of allowed CORS origins |
GROQ_API_KEY |
No | Enables real AI generation; app runs on fallback data without it |
NODE_ENV |
No | production toggles secure cookies, SameSite=None, trust proxy, combined logging |
PORT |
No | Defaults to 5000 |
config/env.js calls process.exit(1) at startup if MONGO_URI or JWT_SECRET is missing, so misconfiguration fails loudly at boot rather than surfacing as confusing runtime errors later.
| Variable | Purpose |
|---|---|
VITE_API_URL |
Base URL the Axios instance targets, e.g. https://your-backend.onrender.com |
cd backend
npm install
# create a .env file with MONGO_URI, JWT_SECRET, and optionally GROQ_API_KEY
npm run devRuns on http://localhost:5000 by default (or PORT if set), using nodemon for auto-restart.
cd frontend
npm install
npm run devRuns on the Vite dev server, default http://localhost:5174 unless configured otherwise, matching the CLIENT_URL default in config/env.js.
- Backend on Render. Node/Express service;
render.yamlin the backend directory suggests infrastructure-as-code config is checked in. Environment variables (MONGO_URI,JWT_SECRET,CLIENT_URL,GROQ_API_KEY,NODE_ENV=production) are set in Render's dashboard. - Frontend on Vercel.
vercel.jsonin the frontend directory configures the deploy;VITE_API_URLpoints at the Render backend URL. - Database. MongoDB, typically MongoDB Atlas's free tier for a project at this scale.
- Both platforms redeploy automatically on push to the main branch.
- No CSRF token layer. Security currently rests entirely on
SameSitecookie policy and the CORS origin allowlist. A stricter setup would add double submit CSRF tokens on top, as defense in depth. - Single active roadmap per user. Generating a new roadmap destroys the old one and its progress. No roadmap history or ability to run multiple learning tracks in parallel.
- No password reset flow. Registration and login only.
- AI output isn't schema validated beyond a regex JSON extraction.
parseJsonFromTextlooks for the first{...}or[...]block in the model's response; a sufficiently malformed response still falls through to the fallback, which is safe, but there's no retry or repair step before giving up. - Chat history has no pagination or size cap. A very long running conversation grows one MongoDB document indefinitely.
- No refresh token rotation. The JWT is long lived (30 days) with no revocation mechanism short of changing
JWT_SECRET, which would log out every user at once. - No cascading deletes. Deleting a
Userleaves theirProfile,Roadmap,Progress,ChatHistory, andSavedProjectdocuments behind as orphaned records.