Official web platform for the Redeemer's University Computer Science Association, centralizing academic resources, events, alumni networking, and facility reporting.

RUNACOS is a full-stack web platform for the Computer Science Association at Redeemer's University, combining a public-facing site (news, events, alumni directory) with a backend fault-reporting system and a full admin management suite. Built with React + Vite on the frontend and Express + Prisma on the backend, with Supabase handling auth and storage, it provides students and faculty a centralized hub for academic resources and community engagement. The platform emphasizes a premium SaaS-like aesthetic — dark theme, gold accents, glassmorphic cards, and smooth animations — serving as a model for university digital infrastructure.
The RUNACOS (Redeemer's University Computer Science Association) platform is a three-tier web application serving as the official digital hub for CS students and staff.
| Component | Role | Technology Stack |
|---|---|---|
| Frontend (Public) | Student-facing website and portal | React 19, TypeScript, Vite, TailwindCSS, Framer Motion, GSAP |
| Admin Dashboard | Staff management interface | React 19, TypeScript, Vite, TailwindCSS, TipTap |
| Backend API | REST API & data layer | Express.js, Node.js, Prisma ORM, Zod validation |
| Database | Persistent data storage | PostgreSQL (Supabase) |
| Auth | User authentication | Supabase Auth (Google OAuth) |
| Caching | Request caching | Upstash Redis |
| Mailing | Email notifications | Nodemailer |
| Storage | Image uploads | Cloudinary |
| API Docs | Swagger/OpenAPI | Swagger UI Express |
Frontend SPA / Admin Panel
│ HTTPS / Bearer token auth
▼
Express.js Backend (Port 3000)
├─ Auth Middleware (Supabase JWT)
├─ Admin Middleware (Role check)
├─ Validation Middleware (Zod)
├─ Cache Middleware (Redis)
└─ 11 Feature Modules
│ Prisma queries
▼
PostgreSQL Database (Supabase)
├─ User accounts (Supabase Auth)
├─ Events & registrations
├─ News articles
├─ FRMS reports & comments
├─ Past questions
└─ Alumni profiles
App.tsx
└─ ThemeProvider > QueryClientProvider > BrowserRouter
└─ AuthProvider
├─ [PUBLIC] Layout
│ ├─ Navbar
│ ├─ HomePage
│ │ ├─ AboutSection / AboutAnimatedBeams
│ │ ├─ EventCard / DepartmentCard
│ │ └─ GalleryGlobe
│ ├─ NewsPage > NewsFeaturedCarousel / NewsStoryCard
│ ├─ EventsPage > EventsGrid > EventCard
│ ├─ EventDetailPage > RegistrationProgress / SocialShareSheet
│ ├─ FRMSPage > FRMSWizard (4-step)
│ │ ├─ CategoryCard / PrioritySlider
│ │ └─ TicketCard (preview)
│ ├─ PastQuestionsPage > FilterSidebar / QuestionCard
│ ├─ AlumniPage > AlumniCard > ExperienceTimeline / AlumniModal
│ ├─ ExecutivesPage > ExecutiveCarousel
│ └─ [Shared UI] GlassCard / BentoGrid / AnimatedCounter / ScrollReveal
├─ [AUTH] LoginPage / AuthCallbackPage
├─ [ONBOARDING] ProtectedRoute > OnboardingPage
└─ [PORTAL] ProtectedRoute > PortalLayout
├─ PortalPage / PortalProfilePage
└─ Events / FRMS / PastQuestions / News (portal variants)
App.tsx
└─ AdminAuthProvider > NotificationProvider
└─ AdminGuard
└─ AdminLayout
├─ Sidebar / TopBar
└─ Pages (Outlet)
├─ DashboardPage (stats + recent activity)
├─ UsersListPage > DataTable > UserEditModal
├─ EventsListPage > DataTable > EventFormModal
├─ NewsListPage > DataTable > NewsFormModal > TipTap Editor
├─ NewsletterPage > CampaignFormModal > TipTap Editor
├─ PastQuestionsListPage > DataTable (approval queue)
├─ FRMSListPage > DataTable > Status/Comment modal
├─ StaffListPage / ExecutivesListPage > DataTable > FormModal
├─ AlumniListPage > DataTable > AlumniFormModal
└─ GalleryMomentsPage > DataTable > CloudinaryImageUploader
Express App
├─ Global: helmet / cors / json / morgan / Redis cache
├─ /api/users — profile sync, role management
├─ /api/events — CRUD + registration + CSV export
├─ /api/pq — past questions + approval queue
├─ /api/news — articles + publish/draft workflow
├─ /api/frms — ticket submission + status tracking
├─ /api/executives — leadership directory
├─ /api/staff — faculty directory
├─ /api/alumni — alumni profiles + mentorship
├─ /api/contact — form submissions inbox
├─ /api/newsletter — subscribers + campaign send
├─ /api/gallery-moments — home page gallery
└─ /api/dashboard — admin stats aggregation
1. User clicks "Sign in with Google"
2. supabase.auth.signInWithOAuth() → Google consent → token
3. Redirect to /auth/callback with session
4. AuthContext validates email domain (@run.edu.ng)
5. Stores access_token in localStorage
6. Fetches/syncs AppUser row via POST /api/users
7. If matricNumber/level missing → redirect to /onboarding
8. User fills onboarding form → PATCH /api/users/:id
9. Redirect to /portal or home
FRMSWizard (4 steps):
Step 1: Category + Title + Description
Step 2: Priority + Location + Evidence upload
Step 3: Submitter info
Step 4: Anonymous toggle + confirmation
↓
POST /api/frms → generate ticketId → Prisma FrmsReport
↓
Admin: FRMSListPage → status update → PATCH /api/frms/:ticketId
| Setting | Value |
|---|---|
| staleTime | 5 minutes |
| gcTime | 10 minutes |
| retry | 1 |
| refetchOnWindowFocus | false |
Cache is invalidated on every POST/PATCH/DELETE via Redis middleware.
| Model | Key Fields |
|---|---|
| User | id (Supabase UUID), email (@run.edu.ng), fullName, matricNumber, level, role, status |
| Event | id, slug, name, type, venue, eventDate, capacity, registrationEnabled, status |
| EventRegistration | id, eventId, userId, registrationStatus |
| NewsArticle | id, title, content (HTML), imageURL, category, isPublished, isFeatured |
| PastQuestion | id, courseCode, level, semester, academicSession, fileURLs, status, downloadCount |
| FrmsReport | id, ticketId, category, priority, status, evidenceURL, submitterEmail |
| FrmsComment | id, reportId, userId, message, statusChange |
| AlumniProfile | id, slug, displayName, graduationYear, jobTitle, company, experience (JSON) |
| NewsletterCampaign | id, subject, bodyHTML, sendTo, status, sentAt |
| Tool | Purpose |
|---|---|
| Vite 7.3 | Frontend bundler & dev server |
| TypeScript 5.9 | Type checking across all packages |
| TailwindCSS 4.2 | Utility CSS |
| Prisma 5.10 | ORM + database migrations |
| Express 4.18 | Backend framework |
| ESLint 9 | Linting |
| Jest 29 | Backend unit testing |
| Nodemon 3 | Dev server hot-reload |
Frontend, admin, and backend are independent package.json workspaces with separate Vercel deployments. This keeps bundle sizes small and allows different DevOps requirements per package, at the cost of some type duplication.
Google OAuth JWTs are issued and verified by Supabase; application state (roles, profiles) lives in Prisma-managed PostgreSQL. This decouples identity from app data but requires an extra Supabase API call per request to verify JWTs.
Each domain (events, news, frms, etc.) has its own routes / controller / service / repository / types / doc files. Self-contained modules make it easy to add or remove features; the trade-off is some cross-module duplication in error handling.
All GET responses are cached in Upstash Redis; any POST/PATCH/DELETE clears the full cache. Simple and effective for a small team, but a blanket invalidation strategy — selective per-endpoint caching would scale better.
TipTap provides a headless rich-text editor for news articles and newsletters; Cloudinary handles image uploads and CDN delivery. Both integrate cleanly with React Hook Form and avoid managing media storage on the backend server.
The fault-reporting form is broken into 4 steps to improve UX for a complex submission — avoiding a single long form reduces errors and increases completion rates.