Building a Full-Stack Campus Placement Management System with React, TypeScript, and Supabase
Introduction
Managing campus placements across students, TPO offices, and administrators requires robust access control, real-time data synchronization, and secure document handling. In this technical deep-dive, we'll explore how we built a comprehensive placement management platform that handles everything from Campus Application Forms (CAF) to placement drives and internship tracking—all while maintaining strict security boundaries through Row Level Security (RLS).
Tech Stack Overview
Our platform leverages modern web technologies for performance, type safety, and developer experience:
- Frontend: Vite + React 18 + TypeScript for blazing-fast builds and compile-time safety
- Styling: Tailwind CSS + shadcn-ui for consistent, accessible components
- Backend: Supabase (PostgreSQL + Auth + Storage + Real-time subscriptions)
- Data Fetching: TanStack Query for intelligent caching and optimistic updates
- State Management: React hooks with server state handled by TanStack Query
Architecture: Role-Based Access Control
Defining User Roles
The system supports three distinct roles defined as a PostgreSQL enum and enforced throughout the database:
CREATE TYPE user_role AS ENUM ('student', 'tpo', 'admin');Each user profile links to auth.users and includes their role, with helper functions providing clean access checks:
get_user_role(): Returns current authenticated user's rolehas_role(role): Boolean check for specific roleis_staff(): Quick check for TPO or admin privileges
Intelligent Route Protection
Authentication flows redirect users based on their role immediately after login:
// Dashboard.tsx routing logic
if (userRole === 'admin') navigate('/admin');
else if (userRole === 'tpo') navigate('/tpo');
// Students remain on /dashboardAdmins can create TPO accounts directly through the platform using Supabase's auth signup flow, maintaining full control over institutional access.
Data Model: The CAF Workflow
Multi-Table CAF Architecture
The Campus Application Form (CAF) is split across seven normalized tables to handle different data categories while maintaining referential integrity:
- caf_forms: Master record with overall status
- caf_personal: Personal and contact information
- caf_documents: Identity and residence proofs
- caf_academics: 10th, 12th, and degree details
- caf_semester_results: Per-semester CGPA tracking
- caf_professional: Skills, projects, certifications
- caf_edit_requests: Tracks TPO feedback and revision cycles
Status State Machine
CAF forms progress through a strict workflow:
draft → submitted → approved/rejected/edit_requested
Students can only edit forms in draft or edit_requested states. Once submitted, TPOs review and either approve, reject, or request edits with specific feedback. This gating mechanism prevents students from accessing placement drives until their CAF is approved.
Placement Drives: Complex Eligibility Logic
Dynamic Eligibility Criteria
Placement drives support granular eligibility rules stored as JSONB:
CREATE TABLE placement_drives (
-- ... other fields
eligibility JSONB DEFAULT '{
"min_cgpa": 6.0,
"max_backlogs": 2,
"min_10th_percentage": 60,
"min_12th_percentage": 60,
"eligible_batches": [2025],
"eligible_program_ids": []
}'::jsonb,
application_deadline TIMESTAMPTZ
);Frontend Eligibility Validation
The student interface fetches both drives and academic records, computing eligibility client-side before allowing applications:
// PlacementDrivesStudent.tsx logic
const isEligible = (drive, academics) => {
return (
academics.cgpa >= drive.eligibility.min_cgpa &&
academics.backlogs <= drive.eligibility.max_backlogs &&
academics.tenth_percentage >= drive.eligibility.min_10th_percentage &&
// ... batch and program checks
new Date() <= new Date(drive.application_deadline)
);
};Applications are tracked in placement_applications with statuses including applied, shortlisted, interviewed, placed, and rejected.
Internship Management with Document Storage
Secure File Uploads
Internships require offer letter uploads stored in Supabase Storage with carefully scoped policies:
-- Storage policies for offer-letters bucket
CREATE POLICY "Public read access"
ON storage.objects FOR SELECT
USING (bucket_id = 'offer-letters');
CREATE POLICY "Owner write access"
ON storage.objects FOR INSERT
USING (bucket_id = 'offer-letters' AND auth.uid()::text = (storage.foldername(name))[1]);Students can upload and update their own letters but cannot modify others'. The student_close_internship RPC allows marking completion dates without exposing direct table access.
Status-Based Edit Locking
Internship applications follow a lifecycle where students submit, TPOs approve/reject, and students can mark end dates. Once finalized (approved/rejected), the form becomes read-only to preserve audit trails.
TPO Dashboard: Bulk Operations and Workflows
CAF Review Interface
TPOs access a dedicated review interface that fetches complete student profiles with all CAF sections joined:
// CAFApprovals.tsx query
const { data: cafSubmissions } = useQuery({
queryKey: ['caf-submissions'],
queryFn: async () => {
const { data } = await supabase
.from('caf_forms')
.select(`
*,
caf_personal(*),
caf_academics(*),
caf_documents(*),
profiles(full_name, email)
`)
.eq('status', 'submitted');
return data;
}
});The review dialog provides inline approve/reject/edit-request actions with feedback comments stored in caf_edit_requests.
Bulk Application Updates
For large placement drives, TPOs can bulk-update application statuses via an RPC that optionally suppresses individual notifications:
CREATE FUNCTION bulk_update_application_status(
app_ids UUID[],
new_status TEXT,
suppress_notifications BOOLEAN DEFAULT false
)This prevents notification spam when marking 50+ applications as shortlisted simultaneously.
Real-Time Notifications System
Trigger-Based Notification Creation
Database triggers automatically create notifications for key events:
-- Trigger on placement_drives insert
CREATE TRIGGER notify_new_placement_drive
AFTER INSERT ON placement_drives
FOR EACH ROW EXECUTE FUNCTION notify_new_drive();Functions check eligibility criteria and create targeted notifications only for applicable students. Notification triggers also fire on CAF status changes and application updates, keeping all users informed without polling.
Frontend Notification Polling
The dashboard polls notifications every 30 seconds using TanStack Query's refetch interval:
const { data: notifications } = useQuery({
queryKey: ['notifications'],
refetchInterval: 30000, // 30 seconds
});For production deployments, this could be replaced with Supabase real-time subscriptions for instant updates.
Security: Row Level Security Patterns
Self-Access vs. Staff Patterns
RLS policies follow consistent patterns across tables:
Students can read/write their own data:
CREATE POLICY "Students view own records"
ON students FOR SELECT
USING (auth.uid() = user_id);Staff can access all records:
CREATE POLICY "Staff view all records"
ON students FOR SELECT
USING (is_staff());Permissive Insert Policies
Initially restrictive insert policies were relaxed to allow students to create their own CAF sections and applications:
-- Migration 20251215145449
CREATE POLICY "Students can insert own CAF data"
ON caf_personal FOR INSERT
WITH CHECK (auth.uid() = user_id);This pattern repeats across caf_* tables and placement_applications, ensuring students control their data while TPOs/admins retain oversight.
Frontend Architecture Patterns
Feature-Based File Organization
src/
├── pages/ # Route components
│ ├── Dashboard.tsx
│ ├── CAFForm.tsx
│ ├── PlacementDrivesStudent.tsx
│ └── TPODashboard.tsx
├── components/
│ ├── tpo/ # TPO-specific components
│ │ ├── CAFApprovals.tsx
│ │ ├── PlacementDrives.tsx
│ │ └── InternshipApprovals.tsx
│ └── ui/ # shadcn-ui primitives
└── lib/
└── supabase.ts # Client initialization
Each dashboard role gets its own page component that composes role-specific sub-components, maintaining clear separation of concerns.
Optimistic Updates with TanStack Query
Form submissions use optimistic updates to provide instant feedback:
const mutation = useMutation({
mutationFn: submitCAF,
onMutate: async (newData) => {
await queryClient.cancelQueries(['caf']);
const previous = queryClient.getQueryData(['caf']);
queryClient.setQueryData(['caf'], newData);
return { previous };
},
onError: (err, newData, context) => {
queryClient.setQueryData(['caf'], context.previous);
},
});This pattern appears throughout the application for CAF updates, drive applications, and internship submissions.
Deployment Considerations
Environment Configuration
The application requires several Supabase environment variables:
VITE_SUPABASE_URL=your-project-url
VITE_SUPABASE_ANON_KEY=your-anon-keyCritical security note: The anon key is safe for client-side use due to RLS policies, but service role keys must never be exposed in frontend code or committed to version control.
Migration Strategy
Supabase migrations are ordered chronologically and build upon each other:
- Base schema with roles and core tables
- RLS policy adjustments for student inserts
- Placement drive eligibility enhancements
- Notification system with triggers
- Internship tables and storage policies
- Bulk operation RPCs
Migrations should be applied in sequence during deployment using supabase db push or equivalent CI/CD tooling.
Performance Optimizations
Database Indexing
Key foreign keys and frequently queried columns should have indexes:
CREATE INDEX idx_caf_forms_user_status ON caf_forms(user_id, status);
CREATE INDEX idx_placement_applications_drive ON placement_applications(drive_id);
CREATE INDEX idx_notifications_user_read ON notifications(user_id, is_read);These indexes significantly speed up dashboard queries that filter by user and status.
Query Complexity Management
Complex joins in TPO dashboards (fetching drives with applications and student profiles) can be expensive. Consider:
- Pagination: Limit initial loads to 20-50 records
- Lazy loading: Load application details on-demand rather than upfront
- Materialized views: Pre-compute dashboard statistics for faster reads
Testing Strategy Gaps
The current implementation lacks automated testing coverage. Production deployments should include:
- Unit tests: Helper functions, eligibility calculations, date validations
- Integration tests: API routes, RLS policy enforcement, trigger behavior
- E2E tests: Critical flows (CAF submission, drive application, TPO approval)
Playwright or Cypress would provide browser-based E2E coverage, while Jest + React Testing Library handle component testing.
Future Enhancements
Real-Time Collaboration
Replace polling with Supabase real-time subscriptions for instant notification delivery:
supabase
.channel('notifications')
.on('postgres_changes', {
event: 'INSERT',
schema: 'public',
table: 'notifications',
filter: `user_id=eq.${userId}`
}, (payload) => {
queryClient.invalidateQueries(['notifications']);
})
.subscribe();Analytics Dashboard
TPOs and admins would benefit from visualizations showing:
- Application conversion rates (applied → placed)
- Program-wise placement statistics
- Company-wise offer distributions
- Historical trends across academic years
Libraries like Recharts (already in the stack) can render these insights.
Mobile App
React Native with Supabase's cross-platform SDK would extend access to mobile, particularly useful for students checking drive updates and notifications on the go.
Conclusion
Building a campus placement system requires careful attention to access control, data integrity, and user experience across multiple roles. By leveraging Supabase's built-in auth, RLS, and real-time capabilities alongside React's component model and TypeScript's type safety, we created a maintainable platform that scales from small colleges to large universities.
The key architectural decisions—role-based routing, normalized CAF storage, RLS-enforced security, and trigger-based notifications—provide a solid foundation for extending the system with additional features like interview scheduling, resume parsing, or third-party ATS integrations.
Whether you're building HR tech, education platforms, or any multi-tenant application with complex workflows, these patterns demonstrate how modern serverless PostgreSQL platforms can replace traditional backend servers while maintaining enterprise-grade security and performance.
Live Demo: OnCampus
Tech Stack: React • TypeScript • Supabase • TanStack Query • Tailwind CSS