/app
├── backend/
│ ├── server.py # FastAPI app, CORS, router registration, startup (indexes + seed)
│ ├── database.py # motor client — db from MONGO_URL + DB_NAME
│ ├── auth_utils.py # bcrypt hash/verify, JWT create/decode, get_current_user, require_role
│ ├── seed.py # idempotent demo data (admin/teacher/student, courses, tests, batches…)
│ ├── notify.py # send_email (Resend), email_template, notify() in-app + email
│ ├── zoom_service.py # Server-to-Server OAuth token cache + create_zoom_meeting
│ ├── uploads/ # uploaded files (lesson notes, submissions)
│ ├── requirements.txt # update ONLY via: pip install X && pip freeze > requirements.txt
│ ├── .env # secrets/config (never commit; not pushed to GitHub)
│ ├── tests/ # pytest suites per iteration
│ └── routers/ # one file per domain:
│ auth.py courses.py tests.py live_classes.py assignments.py
│ announcements.py dashboard.py payments.py batches.py files.py
│ certificates.py notifications.py admin.py
└── frontend/
├── package.json # update ONLY via: yarn add <pkg>
├── .env # REACT_APP_BACKEND_URL, REACT_APP_ACADEMY_NAME
├── public/docs/ # these downloadable documents
└── src/
├── App.js # all routes; Protected wrapper redirects to /auth
├── index.css # fonts (Cabinet Grotesk + IBM Plex Sans), .font-heading, tokens
├── lib/api.js # axios instance (+Bearer interceptor), formatApiError, uploadFile, fileUrl
├── lib/config.js # ACADEMY_NAME branding constant
├── context/AuthContext.jsx
├── components/ # PortalLayout (role-aware nav), EnrollModal, NotificationsBell,
│ # TeacherAnalytics, AdminDashboard, ui/ (shadcn)
└── pages/ # one default-export component per route
# services are supervisor-managed sudo supervisorctl restart backend # after .env changes or pip installs sudo supervisorctl restart frontend # after yarn add tail -n 100 /var/log/supervisor/backend.err.log # backend logs
Backend must stay on 0.0.0.0:8001, frontend on 3000. Every backend route must be prefixed /api (ingress routes /api → 8001, everything else → 3000). The frontend must always call process.env.REACT_APP_BACKEND_URL.
| Key | Purpose |
|---|---|
| MONGO_URL, DB_NAME | MongoDB connection (do not rename) |
| CORS_ORIGINS | Comma-separated origins ("*" default) |
| JWT_SECRET | HS256 signing key for tokens |
| ADMIN_EMAIL / ADMIN_PASSWORD | Admin account seeded at startup |
| STRIPE_API_KEY, RAZORPAY_KEY_ID, RAZORPAY_KEY_SECRET | Payment gateways — empty = demo mode (simulated success) |
| RESEND_API_KEY, SENDER_EMAIL | Email sending — empty key = demo (logs only) |
| FRONTEND_URL | Base URL used in password-reset links |
| ACADEMY_NAME | Brand name used in emails / API title |
| ZOOM_ACCOUNT_ID, ZOOM_CLIENT_ID, ZOOM_CLIENT_SECRET | Zoom S2S OAuth — empty = auto-create disabled |
| REACT_APP_BACKEND_URL | API base (do not hardcode URLs anywhere) |
| REACT_APP_ACADEMY_NAME | Brand name shown across the UI |
_id; every response renames _id → id (d["id"] = d.pop("_id")). Never return raw ObjectIds.Depends(get_current_user) for any-auth, Depends(require_role("teacher","admin")) for role-restricted. Students never receive correct_index (see test_out(hide_answers=True)).datetime.now(timezone.utc).isoformat() strings, except password_reset_tokens.expires_at (native datetime for the TTL index).$in maps or aggregation $group (see teacher_courses, teacher_analytics, list_tests, list_assignments for the pattern).await notify([...ids], title, body, link, email_subject=…, email_html=…) — emails are fire-and-forget via asyncio.create_task; failures are logged, never raised.routers/x.py with router = APIRouter(tags=["x"]), then import + api_router.include_router(x.router) in server.py. Indexes go in the startup event.{ api, formatApiError } from lib/api.js; wrap mutations in try/catch and surface errors with toast.error(formatApiError(e)) (sonner).uploadFile(file) POSTs multipart and returns {id,url,filename}; render links with fileUrl(url) (prefixes backend URL for relative paths)./app/* inside PortalLayout (guarded by Protected); standalone pages (certificate, reset-password) are separate routes. Role-aware nav in PortalLayout (NAV vs ADMIN_NAV).user.role).data-testid (e.g. auth-submit-button, enroll-button-{id}). Keep this for new UI.font-heading for headings, blue-700 primary. No inline hex except tokens already used.gateway_config() flips demo_mode to false.routers/payments.py → confirm_payment (currently returns 501 when keys exist): create a gateway order in checkout, verify signature/webhook in confirm, then reuse the existing enrollment + notification block.meeting:write:admin; activate.GET /api/zoom/config now returns configured:true → the "Auto-create Zoom meeting" checkbox enables itself; created classes get join_url as meeting_link.Verify a domain at resend.com/domains, then set SENDER_EMAIL to an address on that domain. Until then delivery is limited to the Resend account owner.
cd /app/backend && python -m pytest tests/ -v # 59+ API tests across 3 suites
Seeded accounts: admin@jamacademy.com/Admin@123 · teacher@jamacademy.com/Teacher@123 · student@jamacademy.com/Student@123. Historical reports live in /app/test_reports/. Password-reset tests extract the token from backend logs (grep "Password reset link" /var/log/supervisor/backend.err.log).
| Task | Where |
|---|---|
| Rename the academy | frontend/.env REACT_APP_ACADEMY_NAME + backend/.env ACADEMY_NAME, restart both |
| Change seed/demo data | backend/seed.py (idempotent — only inserts when collections are empty) |
| Add a lesson type | LessonBody in routers/courses.py + icon/handling in pages/CourseDetail.jsx |
| Change upload limits/types | routers/files.py → ALLOWED_EXT, MAX_SIZE |
| Add a notification trigger | call notify() from the relevant router (see courses.py enroll for the pattern) |
| Change JWT lifetime | auth_utils.py create_access_token timedelta |
| Add admin capability | routers/admin.py + pages/Admin*.jsx + ADMIN_NAV in PortalLayout |
| Email template styling | notify.py email_template() |
© 2026 Rohini's JAM Academy · Developer Guide v1.0