Academic Roadmap, Notes & Progress Tracker β An AI-powered study app that extracts roadmaps from syllabi, tracks topic completion with confidence ratings, and builds a connected knowledge graph from markdown notes.
- Architecture Overview
- Tech Stack
- Project Structure
- Core Features
- Data Model
- Authentication & Authorization
- AI Extraction Pipeline
- API Endpoints
- Frontend Architecture
- Getting Started
- Environment Configuration
- Development Workflow
- Deployment
- Roadmap
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β TENACITI ARCHITECTURE β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β
β β FRONTEND ββββββΊβ BACKEND ββββββΊβ DATABASE β β
β β (React 19) β β (FastAPI) β β (PostgreSQL) β β
β β + Vite β β + SQLAlchemyβ β (Supabase) β β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β
β β β β β
β β βΌ β β
β β ββββββββββββββββ β β
β β β AI SERVICE β β β
β βββββββββββββΊβ (Groq) βββββββββββββββββ β
β ββββββββββββββββ β
β β β
β βΌ β
β ββββββββββββββββ β
β β STORAGE β β
β β (Cloudflare R2) β
β ββββββββββββββββ β
β β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β
β β AUTH β β BILLING β β REALTIME β β
β β (Supabase β β (LemonSqueezy)β β (Supabase β β
β β Auth) β β β β Realtime) β β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Key Design Principles:
- Hybrid Auth: Supabase Auth (Google OAuth) + Custom JWT (email/password)
- Tier-gated Features: Free vs Pro limits enforced at API layer
- Async AI Processing: Document extraction runs in background with polling
- Knowledge Graph: Wikilinks (
[[Note Title]]) auto-create bi-directional links - Self-Assessment Loop: Submit β Rate β Reflect β Analyze gaps
| Layer | Technology | Version |
|---|---|---|
| Frontend | React + Vite | 19.2 / 8.1 |
| Routing | React Router | 7.18 |
| State | React Context + Hooks | β |
| Drag & Drop | @dnd-kit | 6.3 |
| Graph Viz | react-force-graph-2d | 1.29 |
| Markdown | Custom (textarea + preview) | β |
| Backend | FastAPI | Latest |
| ORM | SQLAlchemy (Async) | 2.0 |
| Migrations | Alembic | Latest |
| Auth | Supabase Auth + Custom JWT | β |
| Rate Limiting | SlowAPI | Latest |
| AI | Groq (Llama 3.x via router) | β |
| Storage | Cloudflare R2 (S3-compatible) | β |
| Database | PostgreSQL (Supabase) | 15+ |
| Billing | LemonSqueezy | β |
| Resend | β | |
| Linting | Oxlint | 1.71 |
Tenaciti/
βββ backend/ # FastAPI Backend
β βββ app/
β β βββ main.py # App entry point, router registration
β β βββ config.py # Pydantic Settings (env-driven)
β β βββ database.py # Async SQLAlchemy engine + session
β β βββ middleware/
β β β βββ auth.py # Hybrid JWT validation (local + Supabase)
β β β βββ rate_limit.py # SlowAPI integration
β β β βββ tier_gate.py # Free/Pro feature guards
β β βββ models/ # SQLAlchemy ORM models
β β β βββ user.py
β β β βββ course.py
β β β βββ document.py
β β β βββ roadmap_node.py
β β β βββ topic.py
β β β βββ topic_completion.py
β β β βββ note.py
β β β βββ note_link.py
β β β βββ goal.py
β β β βββ goal_course.py
β β β βββ gpa_entry.py
β β β βββ self_assessment_log.py
β β β βββ streak.py
β β β βββ streak_daily_log.py
β β β βββ subscription.py
β β βββ routes/ # API endpoints (REST)
β β β βββ auth.py # Register, login, OAuth callback, password reset, email verify
β β β βββ courses.py # CRUD + limits
β β β βββ documents.py # Upload, list, delete, extraction triggers
β β β βββ roadmap_nodes.py # CRUD, confirm, self-assessment submit
β β β βββ topics.py # CRUD, toggle, reorder, merge, link-node, confirm
β β β βββ notes.py # CRUD, search, backlinks, wikilink parsing
β β β βββ goals.py # CRUD + GPA target tracking
β β β βββ gpa.py # GPA entry + calculation
β β β βββ self_assessment.py # Submit, gap analysis
β β β βββ billing.py # LemonSqueezy webhooks + portal
β β β βββ admin.py # Admin utilities
β β βββ schemas/ # Pydantic request/response models
β β βββ services/
β β β βββ auth_service.py # Password hashing, JWT, tokens
β β β βββ storage_service.py # Cloudflare R2 wrapper
β β β βββ extraction_service.py # Topic extraction from PDF/PPTX
β β β βββ roadmap_extraction.py # Assessment extraction from syllabus
β β β βββ groq_router.py # Circuit-breaker LLM router
β β β βββ email_service.py # Resend email sender
β β β βββ gpa_service.py # GPA calculation logic
β β βββ __init__.py
β βββ alembic/ # Database migrations
β βββ requirements.txt
β βββ .env.example
β
βββ web/ # React + Vite Frontend
β βββ src/
β β βββ main.jsx # Entry point
β β βββ App.jsx # Routes + AuthProvider + ProtectedRoute
β β βββ api/client.js # Supabase client + apiFetch wrapper
β β βββ context/
β β β βββ AuthContext.jsx # Auth state, login/logout, token management
β β βββ components/
β β β βββ Layout.jsx # Sidebar + top bar
β β β βββ Sidebar.jsx
β β β βββ Topics/
β β β β βββ TopicList.jsx # Main topic UI (drag, merge, link-node)
β β β β βββ TopicItem.jsx # Inline edit, confidence modal trigger
β β β β βββ TopicMergeModal.jsx
β β β β βββ ConfidenceModal.jsx
β β β βββ Notes/
β β β βββ MarkdownEditor.jsx # Editor + wikilink autocomplete
β β β βββ GraphView.jsx # Force-directed knowledge graph
β β β βββ BacklinksPanel.jsx
β β βββ pages/
β β β βββ DashboardPage.jsx # Course cards, stats, quick actions
β β β βββ CoursePage.jsx # 5 tabs: Overview/Docs/Roadmap/Topics/Notes
β β β βββ NotesPage.jsx # List/Graph/Editor views
β β β βββ GoalsPage.jsx
β β β βββ SelfAssessmentPage.jsx
β β β βββ GPAPage.jsx
β β β βββ ProfilePage.jsx
β β β βββ SettingsPage.jsx
β β β βββ Auth pages (Login, Register, Forgot/Reset, Verify)
β β βββ styles/
β βββ package.json
β βββ vite.config.js
β βββ .env.example
β
βββ test/ # Static HTML prototype (legacy)
βββ docs/ # SRS, API specs
βββ docker-compose.yml # Local Postgres + pgAdmin
βββ README.md # This file
- Create courses with code, semester, academic year, credit hours
- Archive courses; track document upload counts per course
- Semester-aware dashboard filtering
| Feature | Free | Pro |
|---|---|---|
| Uploads per course | 3 | 20 |
| Max file size | 10 MB | 25 MB |
| Syllabus β Roadmap | β | β |
| Slides β Topics | β | β |
| Instructor Notes β Topics | β | β |
Extraction Pipeline:
- Upload β Cloudflare R2 (SHA-256 dedup)
- Background job downloads file
pypdf/python-pptxextracts text- Groq (Llama 3.3 70B β fallback chain) returns structured JSON
- Bulk-insert
RoadmapNodeorTopicrows linked toDocument
- Node Types: Assignment, Quiz, Exam, Project, Lab, Other
- Placeholder Detection: Missing deadline/weight β flagged as placeholder
- Confirm Flow: Student reviews AI extraction β fills gaps β confirms
- Self-Assessment: On submit, log quality (1β5), mood (1β5), actual hours, reflection
- Gap Analytics: Confidence gap (creation vs. submission), hours gap, timeliness
- Extracted from slides/notes or added manually
- Drag-and-drop reorder (@dnd-kit)
- Inline edit title
- Confirm-before-lock pattern (prevents accidental completion)
- Confidence Rating (1β5) on completion β tracks calibration
- Merge Mode: Select multiple β merge into one (preserves completions)
- Link to Roadmap Node: Associate topic with assessment for context
- Markdown editor with toolbar (Bold, Italic, Link, Code)
- Wikilinks:
[[Note Title]]β auto-creates/links notes - Backlinks Panel: Shows all notes linking to current note
- Graph View: Force-directed (react-force-graph-2d)
- Node size β connection degree (Obsidian-style)
- Course-based color coding
- Hover highlights neighbors, dims others
- Course filter dropdown
- Semester goals with optional GPA target
- Link goals to courses
- GPA calculator (letter grade β points, weighted by credits)
- Progress tracking against target GPA
- Google OAuth via Supabase Auth
- Email/Password with local JWT (Argon2id)
- Email verification required for local accounts
- Password reset via secure token (Resend)
- Account lockout after 5 failed attempts (15 min)
- Pro subscription via checkout session
- Webhook syncs
plan+plan_expires_aton User - Tier gates on upload limits, file size, extraction types
User 1βββ¬ββ< Course >βββ¬ββ< Document >ββ< Topic >
β β
β βββ< RoadmapNode >ββ< SelfAssessmentLog >
β
βββ< Note >ββ< NoteLink > (self-referential, bi-directional)
β
βββ< Goal >ββ< GoalCourse >ββ Course
β
βββ< GpaEntry >
β
βββ< Subscription > (LemonSqueezy sync)
Key Relationships:
Topic.source_document_idβ traces AI extraction provenanceTopic.linked_node_idβ connects study content to assessmentTopicCompletionβ per-user completion + confidence ratingNoteLinkβ unique (source, target), no self-linksSelfAssessmentLogβ computed gaps (confidence, hours, deadline)
| Token Type | Audience | Validation |
|---|---|---|
| Local JWT | Email/password users | HS256 + app_secret_key, token_version revocation |
| Supabase JWT | Google OAuth users | HS256 + supabase_jwt_secret, audience=authenticated |
Flow:
Frontend sends Authorization: Bearer <token>
β
βΌ
Backend: get_current_user()
β
βββΊ Try local JWT decode + token_version match
β
βββΊ Try Supabase JWT decode (or API fallback)
β
βΌ
Upsert User (auto-create on first OAuth login)
β
βΌ
get_verified_user() enforces is_email_verified
Security Features:
- Argon2id password hashing (configurable params)
- Token version increment on password reset/logout
- Account lockout (5 failures β 15 min)
- Rate limiting: 10/min login, 5/min register, 3/min forgot password
- CORS restricted to configured origins
# Input: Document (PDF/PPTX) from R2
# Output: List[Topic] linked to Document
1. Download file from R2 (threadpool)
2. Extract text: pypdf (PDF) / python-pptx (PPTX)
3. Select system prompt (syllabus vs slides)
4. Groq Router: Llama-3.3-70B β Llama-3.1-8B β Mixtral (circuit breaker)
5. Parse JSON array of topic strings
6. Bulk insert Topic rows (order_index preserved, is_confirmed=False)
7. Update Document.processing_status# Input: Syllabus PDF
# Output: List[RoadmapNode] with structured assessment data
1. Same download + text extraction
2. Structured prompt β JSON: {nodes: [{title, node_type, deadline, weight_percent, confidence}], warnings}
3. Normalize node_type aliases (homeworkβAssignment, midtermβExam, etc.)
4. is_placeholder = deadline is None OR weight is None
5. Bulk insert RoadmapNode (is_confirmed=False, status=Pending)- Circuit-breaker pattern: tracks consecutive failures per model
- Auto-fallback chain:
llama-3.3-70b-versatileβllama-3.1-8b-instantβmixtral-8x7b-32768 - Configurable timeout (30s) and max tokens
All routes prefixed with /api/v1
| Method | Endpoint | Description |
|---|---|---|
| GET | /auth/me |
Get current user |
| PUT | /auth/me |
Update profile |
| POST | /auth/callback |
Supabase OAuth callback |
| POST | /auth/register |
Email/password register |
| POST | /auth/login |
Email/password login |
| POST | /auth/forgot-password |
Request reset email |
| POST | /auth/reset-password |
Reset with token |
| POST | /auth/verify-email |
Verify email token |
| POST | /auth/resend-verification |
Resend verification |
| Method | Endpoint | Description |
|---|---|---|
| GET | /courses |
List user's courses |
| POST | /courses |
Create course |
| GET | /courses/{id} |
Get course |
| PUT | /courses/{id} |
Update course |
| DELETE | /courses/{id} |
Delete course |
| Method | Endpoint | Description |
|---|---|---|
| POST | /documents/courses/{id}/upload |
Upload file (multipart) |
| GET | /documents/courses/{id} |
List documents |
| DELETE | /documents/{id} |
Delete document |
| POST | /documents/{id}/extract-roadmap |
Trigger roadmap extraction |
| POST | /documents/{id}/extract |
Trigger topic extraction |
| GET | /documents/{id}/extraction-status |
Poll extraction status |
| GET | /documents/{id}/topic-extraction-status |
Poll topic extraction |
| Method | Endpoint | Description |
|---|---|---|
| GET | /roadmap-nodes/courses/{id} |
List roadmap nodes |
| POST | /roadmap-nodes/courses/{id} |
Create node manually |
| GET | /roadmap-nodes/{id} |
Get node |
| PUT | /roadmap-nodes/{id} |
Update node |
| DELETE | /roadmap-nodes/{id} |
Delete node |
| POST | /roadmap-nodes/{id}/confirm |
Confirm placeholder |
| POST | /self-assessment/nodes/{id}/submit |
Submit + self-assess |
| GET | /self-assessment/nodes/{id}/gap |
Get gap analytics |
| Method | Endpoint | Description |
|---|---|---|
| GET | /topics/courses/{id} |
List topics with completion |
| POST | /topics/courses/{id} |
Create topic |
| PUT | /topics/{id} |
Update topic |
| PATCH | /topics/{id}/toggle |
Toggle completion + confidence |
| DELETE | /topics/{id} |
Delete topic |
| POST | /topics/{id}/confirm |
Confirm/unconfirm |
| POST | /topics/bulk-reorder |
Reorder topics |
| POST | /topics/merge |
Merge topics |
| PATCH | /topics/{id}/link-node |
Link/unlink roadmap node |
| GET | /topics/courses/{id}/completion-stats |
Progress stats |
| Method | Endpoint | Description |
|---|---|---|
| GET | /notes |
List all notes |
| GET | /notes/courses/{id} |
List course notes |
| POST | /notes |
Create note |
| GET | /notes/{id} |
Get note + backlinks |
| PUT | /notes/{id} |
Update note (parses wikilinks) |
| DELETE | /notes/{id} |
Delete note |
| GET | /notes/search?q= |
Full-text search |
| GET | /notes/backlinks/{id} |
Get backlinks |
| Method | Endpoint | Description |
|---|---|---|
| GET/POST | /goals |
List/create goals |
| GET/PUT/DELETE | /goals/{id} |
CRUD goal |
| GET | /goals/gpa-status |
GPA goal progress |
| GET/POST | /gpa |
List/create GPA entries |
| Method | Endpoint | Description |
|---|---|---|
| GET | /billing/limits |
Current tier limits |
| POST | /billing/checkout |
Create LemonSqueezy checkout |
| POST | /billing/portal |
Create billing portal session |
| POST | /billing/webhook |
LemonSqueezy webhook |
- AuthContext: User, session, login/logout, token storage (localStorage + Supabase)
- Component-level state:
useState/useReducerfor UI state - Server state:
apiFetchwrapper + manualuseEffectfetching (no React Query yet)
/login β LoginPage
/verify-email β VerifyEmailPage
/verify-email/confirm β VerifyEmailConfirmPage
/forgot-password β ForgotPasswordPage
/reset-password β ResetPasswordPage
/ (protected) β DashboardPage
/courses/:id β CoursePage (5 tabs)
/notes β NotesPage (list)
/notes/:id β NotesPage (editor)
/goals β GoalsPage
/self-assessment β SelfAssessmentPage
/gpa β GPAPage
/profile β ProfilePage
/settings β SettingsPage
TopicList (components/Topics/TopicList.jsx)
- Extraction polling banner
- Progress bar (completed/confirmed/total)
- Toolbar: Add, Merge Mode, Confirm All
- Drag-and-drop (@dnd-kit vertical list)
- Merge selection mode + modal
- Link-node dropdown panel
- Completed topics section (collapsed)
GraphView (components/Notes/GraphView.jsx)
- react-force-graph-2d canvas
- Dynamic node sizing:
baseR + min(degree * 0.7, 4) - Course color palette (7 colors)
- Hover: highlight neighbors, dim others
- Course filter dropdown
- Legend + stats chips
MarkdownEditor (components/Notes/MarkdownEditor.jsx)
- Toolbar: Bold, Italic, Link, Code, Preview toggle
- Wikilink autocomplete (
[[trigger) - Auto-save on blur (debounced)
- Live preview (basic markdown β HTML)
- Python 3.12+
- Node.js 20+
- Docker (optional, for local Postgres)
- Supabase account (Auth + DB + Realtime)
- Cloudflare R2 account (file storage)
- Groq API key (AI extraction)
- LemonSqueezy account (billing)
- Resend account (email)
cd backend
# Create venv
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
# Install deps
pip install -r requirements.txt
# Configure environment
cp .env.example .env
# Edit .env with your credentials (see Environment Configuration)
# Run migrations
alembic upgrade head
# Start server
uvicorn app.main:app --reload --port 8000cd web
# Install deps
npm install
# Configure environment
cp .env.example .env
# Edit .env with Supabase URL/key + API URL
# Start dev server
npm run dev # http://localhost:5173# Start local Postgres + pgAdmin
docker-compose up -d
# Update backend .env DATABASE_URL to point to docker-compose postgres# Supabase
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_ANON_KEY=eyJ...
SUPABASE_SERVICE_ROLE_KEY=eyJ...
SUPABASE_JWT_SECRET=your-jwt-secret-from-supabase
# Database (Supabase Postgres)
DATABASE_URL=postgresql+asyncpg://user:pass@host:5432/dbname
# App
APP_SECRET_KEY=your-32-char-secret
DEBUG=true
# LemonSqueezy
LEMONSQUEEZY_API_KEY=...
LEMONSQUEEZY_WEBHOOK_SECRET=...
LEMONSQUEEZY_STORE_ID=...
LEMONSQUEEZY_VARIANT_ID=...
# Cloudflare R2
R2_ACCOUNT_ID=...
R2_ACCESS_KEY_ID=...
R2_SECRET_ACCESS_KEY=...
R2_BUCKET_NAME=tenaciti-uploads
# AI (Groq)
LLM_API_KEY=gsk_...
LLM_PROVIDER=groq
# Email (Resend)
RESEND_API_KEY=re_...
MAIL_FROM=onboarding@resend.dev
# CORS
CORS_ORIGINS=http://localhost:5173,http://localhost:3000VITE_SUPABASE_URL=https://your-project.supabase.co
VITE_SUPABASE_ANON_KEY=eyJ...
VITE_API_URL=http://localhost:8000/api/v1# Run tests
pytest
# Lint
ruff check .
# Format
ruff format .
# New migration
alembic revision --autogenerate -m "description"
alembic upgrade head# Dev server
npm run dev
# Build
npm run build
# Lint
npm run lint
# Preview build
npm run preview# Install pre-commit
pip install pre-commit
pre-commit install- Set all env vars in platform dashboard
- Run migrations on deploy:
alembic upgrade head - Start command:
uvicorn app.main:app --host 0.0.0.0 --port $PORT
- Build command:
npm run build - Output directory:
dist - Set
VITE_API_URLto production backend URL - Set
VITE_SUPABASE_*vars
- Supabase (managed Postgres) β recommended
- Or any PostgreSQL 15+ with
pgvectorextension (for future semantic search)
- Cloudflare R2 β S3-compatible, zero egress fees
- Configure bucket CORS for direct browser uploads (optional optimization)
- React Query / TanStack Query for server state management
- TypeScript migration (frontend + backend schemas)
- Semantic search on notes (pgvector + embeddings)
- Mobile PWA with offline-first notes
- Collaborative notes (Supabase Realtime + CRDT)
- Spaced repetition scheduler for topics (SM-2 algorithm)
- Calendar sync (Google/Outlook) for roadmap deadlines
- PDF annotation overlay on uploaded documents
- Analytics dashboard (study heatmap, velocity, calibration)
- Multi-tenant (classrooms, study groups)
- AI tutor chat grounded in user's notes + syllabus
- Native mobile (React Native / Expo)
Proprietary β All Rights Reserved
Copyright Β© 2026 Ehsaan Qazi. This software and its documentation are proprietary and confidential. Unauthorized copying, distribution, modification, or use is strictly prohibited