Status: Living document β reflects the codebase as of May 2026
Stack: React 19 + Vite + Tailwind CSS 4 (frontend) Β· Express 5 + TypeScript + MongoDB (backend)
- System Overview
- Directory Structure
- Frontend Architecture
- Backend Architecture
- Authentication & Authorization Flow
- Module Pattern
- Role-Based Access Control (RBAC)
- Rate Limiting Strategy
- File Upload Pipeline
- Push Notification Architecture
- Deployment Topology
The CFC website is a full-stack monorepo with a clear separation of concerns:
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β DNS / CDN β
β (Vercel Edge Network) β
ββββββββββββ¬ββββββββββββββββββββββββββββββββ¬ββββββββββββ
β β
βΌ βΌ
βββββββββββββββββββββββ βββββββββββββββββββββββββββ
β Frontend (React) β β Backend (Express) β
β codeforchangenepalβ β /api/* β
β .com β β deployed as Vercel β
β ββββββΆβ Serverless Function β
β PWA + SW + Push β β β
β Notifications β β βββββββββββββββββββ β
β β β β MongoDB Atlas β β
β Deployed: Vercel β β β (Mongoose ODM) β β
βββββββββββββββββββββββ β βββββββββββββββββββ β
β β
β βββββββββββββββββββ β
β β Cloudinary β β
β β (Media Storage) β β
β βββββββββββββββββββ β
β β
β βββββββββββββββββββ β
β β Redis (Optional)β β
β β (Caching) β β
β βββββββββββββββββββ β
β β
β βββββββββββββββββββ β
β β SMTP (Gmail) β β
β β (Email/OTP) β β
β βββββββββββββββββββ β
βββββββββββββββββββββββββββ
| Layer | Technology | Version | Purpose |
|---|---|---|---|
| Frontend Framework | React | 19.2 | UI rendering |
| Build Tool | Vite | 7.2 | Dev server + bundling |
| CSS Framework | Tailwind CSS | 4.1 | Utility-first styling |
| Animation | Framer Motion | 12.38 | Declarative animations |
| Routing | React Router DOM | 7.13 | Client-side routing |
| HTTP Client | Axios | 1.13 | API communication |
| Charts | Recharts | 3.8 | Admin dashboard charts |
| WYSIWYG Editor | Jodit React | 5.3 | Rich text editing |
| PDF Generation | jsPDF | 4.2 | Certificate/resume export |
| QR Code | React QR Code | 2.18 | Certificate verification |
| Backend Framework | Express | 5.2 | HTTP server |
| Runtime | TypeScript / tsx | 5.9 | Type safety |
| Database ODM | Mongoose | 9.0 | MongoDB object modeling |
| Validation | Zod | 4.2 | Schema validation |
| Authentication | JWT + WebAuthn | β | Auth + biometrics |
| File Upload | Multer + Cloudinary | 2.0 | Image/file hosting |
| Push Notifications | web-push | 3.6 | Web Push API |
| Payment | eSewa API | β | Donation processing |
| Nodemailer | 7.0 | OTP, notifications |
CFC-Official-Website/
β
βββ README.md # Project overview (this doc)
βββ fixing.md # Code review findings
βββ biometrics-login.md # WebAuthn documentation
βββ docs/ # All documentation
β βββ ARCHITECTURE.md # This file
β βββ API.md # API reference
β βββ SETUP.md # Setup guide
β βββ DEPLOYMENT.md # Deployment guide
β βββ CONTRIBUTING.md # Contributor guide
β βββ EDGE_CASES.md # Gotchas & edge cases
β
βββ frontend-cfc/ # React application
β βββ index.html # Entry HTML (PWA meta, splash screens)
β βββ vite.config.js # Vite config (React + Tailwind plugins)
β βββ tailwind.config.js # Tailwind configuration
β βββ eslint.config.js # ESLint flat config
β βββ vercel.json # Vercel deploy config
β βββ package.json # Dependencies & scripts
β βββ public/
β β βββ manifest.json # PWA manifest (13 icon sizes)
β β βββ sw.js # Service worker (push notifications)
β β βββ sitemap.xml # Static sitemap
β β βββ robots.txt # Robots exclusion rules
β β βββ og-image.png # Open Graph image
β β βββ logo.png # CFC logo
β β βββ sajilodigital.png # Sajilo Digital badge
β β βββ Resume/ # Sample resume files
β βββ src/
β βββ main.jsx # App entry (Router + AuthProvider + HelmetProvider)
β βββ App.jsx # Route definitions (all pages)
β βββ App.css # Empty (Tailwind via index.css)
β βββ index.css # Global styles + Tailwind directives
β βββ Context/
β β βββ AuthContext.jsx # Auth state (user, login, logout, passkey, permissions)
β βββ Services/
β β βββ api.jsx # Axios instance (cookie + Bearer fallback)
β βββ Hooks/
β β βββ useScrollToTop.jsx # Scroll reset on route change
β β βββ useFetch.jsx # Generic data fetching hook
β β βββ useDebounce.jsx # Debounced value hook
β β βββ usePWAInstall.js # PWA install prompt hook
β β βββ useProvinceColors.js # Province chart color mapping
β β βββ useResumes.js # Resume CRUD operations hook
β β βββ useImageCompressor.js # Image compression before upload
β βββ Layout/
β β βββ MainLayout.jsx # Header + Footer + children
β β βββ AdminLayout.jsx # Sidebar + TopBar + content
β β βββ AuthLayout.jsx # Centered card layout
β βββ Pages/ # Page components
β β βββ {Home,About,Events,...}.jsx
β β βββ Auth/ # Login, Register, OTP, ResetPassword
β β βββ Admin/ # 18 admin pages (Dashboard, Events, Blogs, Users, etc.)
β β βββ ResumeBuilder/ # Resume dashboard, builder, 3 templates
β βββ Components/
β β βββ Common/ # Header, Footer, PrivateRoute, SEO, Animations, DocxViewer
β β βββ UI/ # Card, Banner, EventCard, CustomCursor, Modal, etc.
β β βββ PageComponents/ # HeroSection, Testimonials, Supporters, EventFilter, etc.
β β βββ Dashboard/ # Admin charts (Recharts components)
β βββ Data/
β β βββ navItems.js # Navigation structure
β β βββ teamData.js # Team member data
β β βββ resumeData.js # Resume template/mock data
β βββ utils/
β βββ pushNotification.js # Service worker registration
β βββ imageCompressor.js # Browser-image-compression wrapper
β
βββ backend-cfc/ # Express + TypeScript backend
βββ package.json # Dependencies & scripts
βββ tsconfig.json # TypeScript config (ES2020, ESNext modules)
βββ vercel.json # Vercel serverless route config
βββ generate-vapid.cjs # VAPID key generator
βββ api/
β βββ index.ts # Vercel serverless entry point
βββ src/
βββ server.ts # Standalone server entry (dev/prod)
βββ app.ts # Express app setup (middleware, routes, error handler)
βββ loaders/
β βββ database.ts # MongoDB connection loader
βββ shared/
β βββ configs/
β β βββ env.ts # Zod-validated environment variables
β β βββ database.ts # Alternative DB connection (for scripts)
β β βββ cloudinary.ts # Cloudinary config
β β βββ redis.ts # Redis client (with mock fallback)
β β βββ permissions.ts # Roles, EB positions, permissions mapping
β βββ middlewares/
β β βββ auth.middleware.ts # JWT verification (cookie + Bearer)
β β βββ role.middleware.ts # Permission-based authorization
β β βββ validate.middleware.ts # Zod body/params/query validation
β β βββ multer.ts # File upload (memory storage, 5MB limit)
β βββ utils/
β βββ appError.ts # Operational error class
β βββ errorHandler.ts # Global error handler
β βββ asyncHandler.ts # Async route wrapper
β βββ jwt.ts # Token generation + verification
β βββ hash.ts # bcrypt password hashing
β βββ mailer.ts # Nodemailer transport (Gmail)
β βββ otp.ts # OTP generation + email sending
β βββ response.ts # Standardized JSON responses
β βββ sendError.ts # Error response helper
β βββ cloudinary.ts # Cloudinary upload/delete/extract
β βββ dns.ts # Email domain MX record verification
β βββ qr.ts # (empty β QR generation in certificate model)
βββ modules/ # 21 feature modules
βββ auth/ # Login, register, OTP, WebAuthn
βββ user/ # User CRUD, permissions, seeding
βββ admin/ # Dashboard, stats, activity logs
βββ events/ # Event CRUD
βββ blogs/ # Blog CRUD
βββ impact/ # Impact stories CRUD
βββ gallery/ # Gallery CRUD
βββ certificates/ # Certificate issue, verify, bulk
βββ donations/ # Donations + eSewa payment
βββ internships/ # Internship CRUD
βββ resources/ # Resource CRUD (role-filtered)
βββ contact/ # Contact form + admin
βββ newsletter/ # Newsletter subscribe + admin
βββ testimonials/ # Testimonial CRUD
βββ supporters/ # Supporter CRUD
βββ team/ # Team member CRUD
βββ periodicals/ # Periodical CRUD
βββ walkthroughs/ # Walkthrough CRUD
βββ resume/ # Resume builder CRUD
βββ notifications/ # Push subscription + admin send
βββ seo/ # Dynamic sitemap + robots.txt
Routes are defined in App.jsx using React Router v7 with three layout groups:
<Routes>
βββ <MainLayout> # Public routes
β βββ / β Home
β βββ /about β About
β βββ /events β Events
β βββ /events/:slug β EventDetails
β βββ /creative β Blog listing
β βββ /creative/:slug β BlogDetail
β βββ /creative/walkthrough β WalkthroughList
β βββ /creative/walkthrough/:slug β WalkthroughDetail
β βββ /creative/periodicals β Periodicals
β βββ /our-impact β OurImpact
β βββ /our-impact/:id β ImpactDetail
β βββ /provinces β Provinces
β βββ /provinces/:provinceName β ProvinceDetails
β βββ /certificate-verification/:token β CertificateVerification
β βββ /internships β Internships
β βββ /internship-application β InternshipApplication
β βββ /donate-us β DonateUs
β βββ /donation-success β DonationSuccess
β βββ /donation-failure β DonationFailure
β βββ /gallery β Gallery
β βββ /resources β Resources
β βββ /contact-us β Contact
β βββ /faq β FAQ
β βββ /register β Register
β βββ /join-us β JoinUs
β βββ * β NotFound
β
βββ <PrivateRoute> + <MainLayout> # Authenticated user routes
β βββ /profile β UserProfile
β βββ /resume-builder β ResumeDashboard
β βββ /resume-builder/:resumeId β ResumeBuilder
β
βββ <AuthLayout> # Auth pages (centered card)
β βββ /login β Login
β βββ /forget-password β ForgetPassword
β βββ /verify-otp β OTPVerify
β βββ /reset-password β ResetPassword
β
βββ <PrivateRoute adminOnly> + <AdminLayout> # Admin routes (18 pages)
βββ /admin/{...} β Dashboard, Events, Blogs, Users, etc.
</Routes>
The application uses React Context for auth state (AuthContext) rather than a full state management library. This is appropriate because:
- Auth state is the only globally shared state
- All other data is fetched per-page via custom hooks (
useFetch,useResumes) - The admin dashboard fetches fresh data on mount (no stale cache concerns)
All 18 admin pages and 3 resume builder pages are lazy-loaded using React.lazy() + Suspense:
const Dashboard = lazy(() => import("./Pages/Admin/Dashboard"));This ensures the initial bundle only contains public pages, keeping the first-load bundle small. Admin pages are fetched on-demand when the user navigates to /admin/*.
- Manifest:
public/manifest.jsonβ 13 icon sizes, theme color#0076B4,display: standalone - Service Worker:
public/sw.jsβ handlespushevents (notification display) andnotificationclickevents (URL navigation) - iOS Support: Splash screens for 8 device sizes (iPhone 16 Pro Max, iPhone SE, iPad Pro 13" etc.)
- Install Prompt:
usePWAInstall.jshook manages thebeforeinstallpromptevent
HTTP Request
β
βΌ
Vercel Serverless (api/index.ts)
β
βΌ
app.ts Middleware Stack
βββ 1. helmet() β Security headers
βββ 2. cors() β CORS with allowed origins
βββ 3. express.json() β Body parsing (2MB limit)
βββ 4. express.urlencoded() β URL-encoded body parsing
βββ 5. mongoSanitize() β NoSQL injection prevention
βββ 6. xssSanitize() β Custom XSS stripping (script tags, event handlers)
βββ 7. cookieParser() β Cookie parsing
βββ 8. hpp() β HTTP parameter pollution protection
βββ 9. compression() β Gzip/brotli compression
βββ 10. morgan("dev") β Request logging (dev only)
βββ 11. Rate Limiters β Auth, registration, password reset, WebAuthn
β
βΌ
Module Router (e.g., event.route.ts)
βββ Optional: authenticate β JWT verification
βββ Optional: requirePermission β RBAC check
βββ Optional: validate β Zod schema validation
βββ Optional: upload β Multer file handling
β
βΌ
Controller
β (thin layer: extracts params, calls service, sends response)
βΌ
Service
β (business logic, database operations)
βΌ
Mongoose Model β MongoDB Atlas
Middlewares are registered in a specific order for security:
- CORS must come before route handlers to handle preflight
- Security middlewares (helmet, mongoSanitize, xssSanitize, hpp) come before body parsing to sanitize input early
- Rate limiters are applied after parsing but before route handlers
- Auth middleware is applied per-route (not globally), allowing public endpoints
- Global error handler is registered LAST β this is critical
const xssSanitize = (req, _res, next) => {
if (req.body) {
req.body = stripHtml(req.body);
}
next();
};This recursive function strips:
<script>blocks (with arbitrary attributes)- All HTML tags via regex
javascript:protocol strings- Event handler attributes (
onclick=,onerror=, etc.)
Note: This is a custom implementation. The
xss-cleanpackage is inpackage.jsonbut is not used in the middleware chain.
User Frontend Backend MongoDB
β β β β
β Enter email+password β β β
βββββββββββββββββββββββββΆβ β β
β β POST /auth/login β β
β βββββββββββββββββββββββββββΆβ β
β β β Find user by email β
β β ββββββββββββββββββββββββββΆβ
β β βββββββββββββββββββββββββββ
β β β β
β β β Check: β
β β β βββ isActive? β
β β β βββ isVerified? β
β β β βββ lockUntil? β
β β β βββ password match? β
β β β β
β β β Generate JWT β
β β β Set HttpOnly cookie β
β β β β
β ββββββββββββββββββββββββββββ β
β β Response: { token, user } β
ββββββββββββββββββββββββββ β β
β β β β
β localStorage.setItem( β β β
β "token", token) β β β
β localStorage.setItem( β β β
β "user", userJSON) β β β
User Frontend Backend Browser
β β β β
β Click "Sign in with β β β
β passkey" β β β
βββββββββββββββββββββββββΆβ β β
β β POST /webauthn/ β β
β β login-options β β
β βββββββββββββββββββββββββββΆβ β
β β β Generate challenge β
β β β Store in memory (60s) β
β ββββββββββββββββββββββββββββ β
β β β β
β β startAuthentication() β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββΆβ
β β β β
β Native biometric β β β
β prompt (Face ID / β β β
β fingerprint) β β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β β β
β β POST /webauthn/ β β
β β login-verify β β
β βββββββββββββββββββββββββββΆβ β
β β β Verify signature with β
β β β stored public key β
β β β Same JWT as password β
β ββββββββββββββββββββββββββββ β
ββββββββββββββββββββββββββ β β
The system uses a dual token strategy due to cross-origin deployment constraints:
- HttpOnly Cookie (
jwt) β Primary auth mechanism. Secure, XSS-proof. Works when frontend and backend are on the same domain. - Bearer Token (
localStorage) β Fallback for cross-origin deployments (e.g., Vercel frontend + separate API host). Vulnerable to XSS.
The auth middleware checks both:
const token = req.cookies?.jwt || req.headers.authorization?.split(" ")[1];
β οΈ Security Concern: SeeEDGE_CASES.mdfor details on this dual strategy.
The authenticate middleware:
- Extracts JWT from cookie or Authorization header
- Verifies token signature + expiry
- Decodes payload (id, email, role)
- Attaches decoded user to
req.user - Background: Updates
lastActivetimestamp (throttled to once per minute per user) - On failure: returns
401JSON response
Every backend module follows a consistent 6-file pattern:
modules/{feature}/
βββ {feature}.interface.ts β TypeScript interfaces / enums
βββ {feature}.model.ts β Mongoose schema + model
βββ {feature}.validation.ts β Zod validation schemas
βββ {feature}.service.ts β Business logic
βββ {feature}.controller.ts β Request/response handling
βββ {feature}.route.ts β Route definitions (wired in app.ts)
Some modules omit files when not needed (e.g., read-only modules may skip validation).
// event.interface.ts β defines IEvent, EventType enum, ISpeaker, IContactInfo
// event.model.ts β Mongoose schema with pre-save slug generation
// event.validation.ts β createEventSchema, updateEventSchema (Zod)
// event.service.ts β CRUD operations with Cloudinary upload
// event.controller.ts β Thin wrappers calling service methods
// event.route.ts β 6 routes (GET 3, POST 1, PUT 1, DELETE 1)// All routes are registered centrally:
app.use("/api", eventRoutes); // GET /api/events
app.use("/api", blogRoutes); // GET /api/blogs
app.use("/api/auth", authRoutes); // POST /api/auth/login
app.use("/api/auth/webauthn", webauthnRoutes); // POST /api/auth/webauthn/...
app.use("/api/users", userRoutes); // GET /api/users/list-user
app.use("/api/resumes", resumeRoutes); // GET /api/resumes
app.use("/api/notifications", notificationRoutes); // POST /api/notifications/subscribe
// ... etc.| Role | Slug | Description |
|---|---|---|
| Super Admin | superadmin |
Full system access |
| Admin | admin |
All permissions |
| Executive Body | eb |
Management team (12 positions) |
| College Representative | cr |
College-level leads |
| General Member | gm |
Regular members |
| IPPL | ippl |
Internship/project leads |
| Advisor | advisor |
Advisory board |
| Alumni | alumni |
Former members |
| Guest | guest |
Unregistered users (default) |
tech-lead, project-lead, vice-project-lead, operation-lead,
admin-lead, hr-lead, pr-lead, treasurer, vice-treasurer,
executive-member, secretary, vice-secretary
Permissions are string-based with a resource:action convention:
PERMISSIONS = {
MEMBER_CREATE: "member:create",
MEMBER_UPDATE: "member:update",
EVENT_CREATE: "event:create",
BLOG_CREATE: "blog:create",
// ... 50+ permissions total
}// For each user, permissions are resolved as:
finalPermissions = rolePermissions βͺ userSpecificPermissions
// Role permissions are predefined in ROLE_PERMISSIONS
// Specific permissions can be added per-user via admin panel
// Admin/SuperAdmin bypass permission checks entirely
// EB tech-lead position also gets admin-level accessRequest β authenticate β requireAnyPermission(PERMISSION_X) β next()
requireAnyPermission:
1. Check if user role is admin/superadmin β skip to next()
2. Check if user is EB tech-lead β skip to next()
3. Merge role permissions + user-specific permissions
4. Check if any required permission is in the merged set
5. If yes β next(); If no β 403
The backend applies 7 distinct rate limiters:
| Limiter | Route | Window | Max Requests | Purpose |
|---|---|---|---|---|
| Global | /api/* |
1 hour | 1000 | General API abuse |
| Auth | /auth/login, /verify-otp |
15 min | 5 | Brute force protection |
| WebAuthn | /webauthn/login-* |
15 min | 10 | Biometric brute force |
| Registration | /auth/register |
1 hour | 3 | Spam registration |
| Password Reset | /auth/forget-password, /resend-otp |
1 hour | 5 | Reset abuse |
| Contact Form | /contacts |
1 hour | 3 | Spam messages |
| Donation | /donations, /donations/initiate-esewa |
15 min | 5 | Payment abuse |
| Newsletter | /newsletter/subscribe |
1 hour | 2 | Subscription spam |
| Internship App | /internships/applications |
15 min | 3 | Application spam |
All limiters use
express-rate-limitwith standard headers and IP-based tracking. Requiresapp.set("trust proxy", 1)for accurate client IP detection behind proxies.
User Upload
β
βΌ
Multer (memoryStorage)
β - 5MB file size limit
β - Allowed: JPEG, PNG, WEBP, PDF, DOC/DOCX
β - Rejects everything else with clear error
βΌ
Controller
β - Receives req.file.buffer
βΌ
Cloudinary Upload
β - 120-second timeout
β - Resource type: "auto"
β - Organized into folders: cfc/{module_name}/
βΌ
Response
βββ secure_url (HTTPS URL)
βββ public_id (for deletion)
βββ folder path
All uploads are organized under cfc/{module}/:
cfc/
βββ events/
βββ profiles/
βββ blogs/
βββ team/
βββ impact/
βββ resources/
βββ certificates/
βββ gallery/
βββ internships/
βββ testimonials/
βββ supporters/
βββ periodicals/
βββ walkthroughs/
Admin Panel (Frontend) User Device
β β
β "Send Notification" β
β (target: all users / role / province) β
ββββββββββββββββββββββΆ β
β β β
β β β
β POST /api/notifications/admin/send
β β β
β βΌ β
β Backend Notification Service β
β β β
β Query users matching filter β
β (pushSubscriptions not empty)β
β β β
β For each user: β
β webpush.sendNotification() β
β β β
β β β
β β Push Service (Browser)
β ββββββββββββββββββββββ
β β β
β β Service Worker β
β β (sw.js) β
β β "push" event β
β β showNotification β
β β β
β β User taps β
β β "notificationclick" β
β β β openWindow(url) β
β β β
1. User logs in
2. Frontend subscribes via Push API (navigator.serviceWorker)
3. POST /api/notifications/subscribe β saves to user.pushSubscriptions
4. Server sends welcome notification
5. On invalid subscription (410 Gone): server auto-removes from DB
6. User can manage preferences via POST /api/notifications/preferences
ββ User ββββββββββββββββββββββββββββββββββββββββββββββββββ
β https://codeforchangenepal.com β
β https://codeforchange.sajilodigital.com.np β
ββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββ
β
DNS (Vercel)
β
ββββββββββββββ΄βββββββββββββ
βΌ βΌ
Frontend (Static) Backend (Serverless)
Vercel SPA ββββββββββββββββββββ
βββββββββββββββ β api/index.ts β
β dist/ β β β
β index.html βββββ /api/* ββββΆβ app.ts β
β assets/* β β MongoDB Atlas β
β sw.js β β Cloudinary β
βββββββββββββββ β SMTP (Gmail) β
ββββββββββββββββββββ
http://localhost:5173 ββββ /api/* βββββΆ http://localhost:5000
(Frontend dev server) (Backend dev server)
MongoDB Atlas (remote)
Cloudinary (remote)
The application uses MongoDB with 13+ collections:
| Collection | Module | Key Fields |
|---|---|---|
users |
Auth/User | name, email, role, password, permissions, webauthnCredentials, pushSubscriptions |
events |
Events | title, description, date, location, type, speakers, status |
blogs |
Blogs | title, content, author, category, slug, isFeatured, seo |
impacts |
Impact | title, description, category, metrics, image |
certificates |
Certificates | certificateId, tokenHash, qrCode, recipientName, status |
donations |
Donations | donorName, amount, paymentMethod, transactionId, status |
internships |
Internships | title, companyName, category, description, status |
applications |
Internships | applicantName, email, internshipId, resume, status |
contacts |
Contact | name, email, subject, message, isRead |
newsletters |
Newsletter | email, isSubscribed, subscribedAt |
gallery |
Gallery | title, imageUrl, category, province |
testimonials |
Testimonials | name, role, content, rating, isActive |
supporters |
Supporters | name, logo, website, tier, isActive |
team |
Team | name, role, position, image, description |
periodicals |
Periodicals | title, slug, category, files, publishedAt |
walkthroughs |
Walkthroughs | title, slug, category, image, files |
resources |
Resources | title, description, fileUrl, category, allowedRoles |
resumes |
Resumes | userId, title, templateId, personalInfo, sections |
logs |
Admin | userId, action, resource, details, createdAt |
tasks |
Admin | title, description, status, priority |
Counter |
Certificates | (auto-increment counter for cert IDs) |
- Operational Errors (
AppError): Expected errors (validation failure, not found, auth failure). Caught byasyncHandlerand forwarded to global error handler. - Programmer Errors: Unexpected bugs (null reference, type errors). Also caught by global handler, returned as 500.
- Global Error Handler: Logs error details (message, stack, timestamp) and returns standardized JSON response.
- API Interceptor: Axios response interceptor catches 401 responses and auto-clears auth state
- Toast Notifications:
react-hot-toastfor user-facing success/error messages - Error Boundaries: (Not implemented β consider adding for admin pages)
| Decision | Rationale | Trade-off |
|---|---|---|
| MongoDB over SQL | Flexible schema for user profiles, nested documents (education, membership) | No joins, denormalized data |
| Monorepo | Single repo for both frontend and backend simplifies CI/CD | Larger clone, independent deploy cycles |
| Vercel Serverless | Zero DevOps, auto-scaling, CDN | Cold starts, 10s function timeout |
| In-memory challenge store | Simple, zero dependencies for WebAuthn | Lost on restart, max ~1K concurrent users |
| Custom XSS sanitizer | Avoids npm dependency for simple use case | Less robust than DOMPurify |
| HttpOnly cookie + Bearer token | Works in both same-origin and cross-origin setups | Dual attack surface |
| Memory storage + Cloudinary | No local file management, CDN delivery | Requires internet for uploads |