DEV Community

Cover image for Building a Full-Stack Class Dues Tracker: Lessons from Real-World Problem Solving
Wishot Cipher🎭
Wishot Cipher🎭

Posted on

Building a Full-Stack Class Dues Tracker: Lessons from Real-World Problem Solving

Introduction: When Campus Problems Meet Code

Picture this: It’s the first semester at university, and chaos reigns. Students don’t know who’s paid their class dues, the class treasurer has receipts scattered across three WhatsApp groups, and there’s zero accountability for how funds are spent. Sound familiar?

This was the exact problem that pushed me to build Class Dues Tracker — a comprehensive payment management system that transformed how our department handles finances. But more importantly, it became my crash course in full-stack development, teaching me lessons no tutorial could.

In this post, I’ll share my journey building this project from scratch, the technical challenges I faced, the solutions I discovered, and most importantly — what I learned about being a developer.


🎯 The Problem: More Than Just Tracking Payments

Before diving into code, I had to understand the real problem. It wasn’t just about tracking who paid — it was about:

  • Trust: Students wanted transparency on where their money went
  • Accountability: Admins needed proof of payments and spending
  • Efficiency: Manual tracking via spreadsheets was error-prone and slow
  • Security: Financial data needed proper protection
  • Accessibility: Both students and admins needed easy access from any device

This taught me my first major lesson: Great software solves human problems, not technical ones.


🏗️ Architecture Decisions: Why I Chose This Stack

The Tech Stack

Frontend: React 18 + TypeScript + Vite
Styling: Tailwind CSS + Framer Motion
Backend: Supabase (PostgreSQL + Auth + Storage)
Deployment: Vercel (Frontend) + Supabase Cloud (Backend)
Enter fullscreen mode Exit fullscreen mode

Why This Stack?

1. React + TypeScript

  • TypeScript saved me countless hours of debugging. Type errors caught at compile-time rather than runtime were game-changers.
  • React’s component model made the UI incredibly maintainable.

Lesson Learned: Type safety isn’t overhead — it’s a productivity multiplier.

2. Supabase Over Building a Custom Backend

I initially considered building a Node.js/Express backend. Here’s why I chose Supabase instead:

// What I avoided building from scratch:
// - Authentication system with bcrypt
// - PostgreSQL database setup
// - File upload infrastructure
// - Real-time subscriptions
// - Row Level Security
// - Automated backups
Enter fullscreen mode Exit fullscreen mode

Lesson Learned: Don’t reinvent the wheel. Use managed services for commodity features so you can focus on your unique value proposition.

3. Tailwind CSS for Rapid UI Development

Coming from vanilla CSS, Tailwind felt weird at first:

// Old me would write:
<div className="payment-card">

// New me embraces utility classes:
<div className="bg-white/10 backdrop-blur-lg rounded-2xl p-6 shadow-xl">
Enter fullscreen mode Exit fullscreen mode

But once I got the hang of it, my development speed 10x’d. No more switching between CSS files and components.

Lesson Learned: Modern CSS frameworks aren’t just trendy — they genuinely make you faster.


🔐 Security: The Hardest Lessons Come from Mistakes

Mistake #1: Trusting the Frontend

My initial code looked like this:

// ❌ WRONG: Checking permissions on the client
if (user.isAdmin) {
  await supabase.from('payments').update({ status: 'approved' })
}
Enter fullscreen mode Exit fullscreen mode

A user could just open DevTools, set user.isAdmin = true, and approve their own payment!

The Fix: Row Level Security (RLS)

-- ✅ RIGHT: Enforce permissions at the database level
CREATE POLICY "Only admins can approve payments"
ON payments FOR UPDATE
USING (
  EXISTS (
    SELECT 1 FROM admins 
    WHERE admins.student_id = auth.uid()
  )
);
Enter fullscreen mode Exit fullscreen mode

Lesson Learned: Never trust the client. Security must be enforced at the database level.

Mistake #2: Exposing Sensitive Data

I was returning full student objects, including password hashes:

// ❌ WRONG: Returning everything
const students = await supabase.from('students').select('*')
Enter fullscreen mode Exit fullscreen mode

The Fix: Explicit field selection

// ✅ RIGHT: Only return what's needed
const students = await supabase
  .from('students')
  .select('id, reg_number, full_name, email, level')
Enter fullscreen mode Exit fullscreen mode

Lesson Learned: The principle of least privilege applies to data too. Only expose what’s absolutely necessary.

Mistake #3: Unvalidated File Uploads

Users were uploading any file type, including executables:

// ❌ WRONG: No validation
await supabase.storage.from('receipts').upload(file.name, file)
Enter fullscreen mode Exit fullscreen mode

The Fix: Strict validation

// ✅ RIGHT: Validate type and size
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'application/pdf']
const MAX_SIZE = 5 * 1024 * 1024 // 5MB

if (!ALLOWED_TYPES.includes(file.type)) {
  throw new Error('Invalid file type')
}
if (file.size > MAX_SIZE) {
  throw new Error('File too large')
}
Enter fullscreen mode Exit fullscreen mode

Lesson Learned: Validate everything. Users will break things in ways you never imagined.


💾 Database Design: Evolution Through Iterations

Version 1: The Naive Schema

My first attempt was too simple:

CREATE TABLE payments (
  id UUID PRIMARY KEY,
  student_id UUID,
  amount DECIMAL,
  paid BOOLEAN
);
Enter fullscreen mode Exit fullscreen mode

Problems emerged immediately:

  • No audit trail (when was it paid? who approved it?)
  • No way to track different payment types
  • No transaction references for verification
  • No rejection reasons

Version 2: The Over-Engineered Schema

I then went to the opposite extreme, creating 8+ tables with complex relationships. Queries became nightmares.

Version 3: The Balanced Schema (Final)

CREATE TABLE payment_types (
  id UUID PRIMARY KEY,
  title VARCHAR(255),
  amount DECIMAL(10,2),
  deadline TIMESTAMP,
  bank_name VARCHAR(255),
  account_number VARCHAR(50),
  target_levels TEXT[],  -- Elegant solution for multi-level targeting
  is_active BOOLEAN DEFAULT true
);

CREATE TABLE payments (
  id UUID PRIMARY KEY,
  student_id UUID REFERENCES students(id),
  payment_type_id UUID REFERENCES payment_types(id),
  amount DECIMAL(10,2),
  transaction_ref VARCHAR(100) UNIQUE,
  receipt_url TEXT,
  status VARCHAR(20) CHECK (status IN ('pending', 'approved', 'rejected')),
  notes TEXT,
  rejection_reason TEXT,
  created_at TIMESTAMP DEFAULT NOW(),
  approved_at TIMESTAMP,
  approved_by UUID REFERENCES admins(student_id)
);
Enter fullscreen mode Exit fullscreen mode

Lesson Learned: Database design is about finding the sweet spot between simplicity and functionality. Start simple, iterate based on real needs.


🎨 UI/UX: Building Trust Through Design

The Glassmorphism Trend

I implemented a glassmorphism design system:

const GlassCard = ({ children, className = '' }) => (
  <div className={`
    bg-white/10 
    backdrop-blur-lg 
    rounded-2xl 
    border border-white/20 
    shadow-xl 
    ${className}
  `}>
    {children}
  </div>
)
Enter fullscreen mode Exit fullscreen mode

Why it worked:

  • Modern, trustworthy appearance
  • Excellent visual hierarchy
  • Performance: CSS filters are GPU-accelerated

Lesson Learned: Design isn’t just aesthetics — it’s communication. A professional UI builds trust, especially for financial applications.

Progress Visualization

Instead of just showing “25/50 paid”, I built a visual progress system:

<div className="relative">
  <div className="absolute inset-0 bg-gray-700 rounded-full" />
  <div 
    className="relative h-4 bg-gradient-to-r from-primary to-accent rounded-full transition-all duration-700"
    style={{ width: `${(paidCount / totalCount) * 100}%` }}
  />
  <div className="absolute inset-0 flex items-center justify-center text-xs font-bold">
    {paidCount}/{totalCount}
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

The Impact: Students became more motivated to pay when they could see their classmates’ progress.

Lesson Learned: Visualizations aren’t just pretty — they drive behavior.


🐛 The Debugging Saga: My Biggest Technical Challenge

The Ghost Payment Bug

The most frustrating bug I encountered:

Symptom: Payments would submit successfully but wouldn’t appear in the admin review page.

4 Hours of Debugging Later…

// The culprit was in my query:
const { data } = await supabase
  .from('payments')
  .select('*')
  .eq('status', 'pending')
  .eq('payment_type_id', selectedType)  // This was undefined!

// The fix:
const { data } = await supabase
  .from('payments')
  .select(`
    *,
    student:students(full_name, reg_number),
    payment_type:payment_types(title, amount)
  `)
  .eq('status', 'pending')
Enter fullscreen mode Exit fullscreen mode

What I Learned:

  1. Silent failures are the worst — Add proper error logging
  2. Use TypeScript enums for status values to catch typos
  3. Supabase’s join syntax is powerful but requires practice
  4. Test with real data, not just mock data

Lesson Learned: The debugger is your friend. console.log is good, but stepping through code is better.


🚀 Performance Optimizations: Making It Fast

Problem: Dashboard Was Slow

Initial load time: 3.2 seconds 😱

Optimization 1: Lazy Loading

// Before: All pages loaded upfront
import DashboardPage from './pages/DashboardPage'
import PaymentDetailPage from './pages/student/PaymentDetailPage'

// After: Code splitting with React.lazy
const DashboardPage = lazy(() => import('./pages/DashboardPage'))
const PaymentDetailPage = lazy(() => import('./pages/student/PaymentDetailPage'))
Enter fullscreen mode Exit fullscreen mode

Result: Initial bundle size reduced by 60%

Optimization 2: Smart Data Fetching

// Before: Fetching everything
const payments = await supabase.from('payments').select('*')

// After: Only fetch what's needed
const recentPayments = await supabase
  .from('payments')
  .select('id, amount, status, created_at')
  .order('created_at', { ascending: false })
  .limit(5)
Enter fullscreen mode Exit fullscreen mode

Optimization 3: Caching Payment Types

// Before: Fetching payment types on every render
useEffect(() => {
  fetchPaymentTypes()
}, [])

// After: Cache for 5 minutes
const [paymentTypes, setPaymentTypes] = useState<PaymentType[]>([])
const [lastFetch, setLastFetch] = useState<number>(0)

useEffect(() => {
  const now = Date.now()
  if (now - lastFetch > 5 * 60 * 1000) {
    fetchPaymentTypes()
    setLastFetch(now)
  }
}, [lastFetch])
Enter fullscreen mode Exit fullscreen mode

Final Result: Dashboard load time: 0.8 seconds

Lesson Learned: Performance optimization is about measuring first, optimizing second. Don’t optimize prematurely.


📱 Real-Time Features: The Magic of Subscriptions

One of the coolest features was real-time notifications:

useEffect(() => {
  const channel = supabase
    .channel('notifications')
    .on(
      'postgres_changes',
      {
        event: 'INSERT',
        schema: 'public',
        table: 'notifications',
        filter: `recipient_id=eq.${user.id}`
      },
      (payload) => {
        // Play sound
        playNotificationSound()

        // Show toast
        toast.success(payload.new.title)

        // Update UI
        setNotifications(prev => [payload.new, ...prev])
      }
    )
    .subscribe()

  return () => {
    supabase.removeChannel(channel)
  }
}, [user.id])
Enter fullscreen mode Exit fullscreen mode

User Experience: When an admin approves a payment, the student instantly gets a notification with sound.

Lesson Learned: Real-time features create magical user experiences that make your app feel alive.


🧪 Testing: The Wake-Up Call

My Testing Journey

Stage 1: No tests, just YOLO deployment

  • Result: Production bugs, angry users

Stage 2: Wrote tests after the fact

  • Result: Tests were a pain to write for existing code

Stage 3: Test-Driven Development (TDD)

  • Result: Caught bugs before they reached production

A Testing Example

// paymentService.test.ts
describe('Payment Submission', () => {
  it('should reject payments with invalid transaction refs', async () => {
    const payment = {
      studentId: '123',
      paymentTypeId: '456',
      transactionRef: '', // Invalid: empty
      amount: 5000
    }

    await expect(submitPayment(payment))
      .rejects
      .toThrow('Transaction reference is required')
  })

  it('should prevent duplicate transaction references', async () => {
    const payment1 = { transactionRef: 'TRX123', /*...*/ }
    const payment2 = { transactionRef: 'TRX123', /*...*/ }

    await submitPayment(payment1)

    await expect(submitPayment(payment2))
      .rejects
      .toThrow('Transaction reference already exists')
  })
})
Enter fullscreen mode Exit fullscreen mode

Lesson Learned: Tests aren’t optional for production apps. They’re your safety net when refactoring.


🎓 Soft Skills: The Unexpected Lessons

Communication > Code

The best code in the world is useless if users don’t understand it.

What I Did:

  • Created detailed documentation with screenshots
  • Recorded video tutorials for admins
  • Set up a feedback channel in our class WhatsApp group
  • Conducted user testing sessions with 5 students

Result: 80% user adoption in the first week.

Lesson Learned: Developer tools are for developers. User-facing products need user-focused communication.

Managing Scope Creep

Midway through, classmates kept requesting features:

  • “Can we add SMS notifications?”
  • “Can we integrate with mobile money?”
  • “Can we track individual expenses?”

I learned to say: “Great idea! Let’s add it to the roadmap for v2.”

Lesson Learned: Shipping a good v1 beats building the perfect product that never launches.

Handling Criticism

When I launched the beta, feedback was… harsh:

  • “The UI is confusing”
  • “Why do I need to upload a receipt?”
  • “The old way was simpler”

Instead of getting defensive, I:

  1. Listened and took notes
  2. Identified common complaints
  3. Fixed the top 3 issues within 48 hours
  4. Re-launched with improvements

Result: Sentiment shifted from skeptical to supportive.

Lesson Learned: Criticism is a gift. It’s free user research.


📊 Impact: By The Numbers

After 1 semester of use:

Metric Before After Improvement
Payment Collection Rate 65% 94% +45%
Average Collection Time 6 weeks 2 weeks -67%
Financial Disputes 12 1 -92%
Admin Time Spent 8 hrs/week 1 hr/week -87%
Student Satisfaction 3.2/5 4.6/5 +44%

Most Rewarding Moment: When the class president told me, “This changed everything. We can finally focus on events instead of chasing payments.”


🔮 What’s Next: Version 2.0

Features I’m planning:

  1. Expense Tracking Module
  2. Admins can log expenses
  3. Students see where money goes
  4. Generate financial reports
  5. Mobile App
  6. React Native version
  7. Push notifications
  8. Offline mode
  9. Analytics Dashboard
  10. Payment trends
  11. Collection forecasting
  12. Export reports
  13. Integration Features
  14. Mobile money APIs
  15. Email notifications
  16. SMS reminders

🎯 Key Takeaways for Aspiring Developers

1. Start with the Problem, Not the Tech

Don’t ask “What cool tech can I use?” Ask “What problem am I solving?”

2. Ship Fast, Iterate Faster

My first version was buggy. That’s okay. Real users give real feedback.

3. Security Can’t Be an Afterthought

Build it in from day one. Retrofitting security is painful.

4. Document Everything

Your future self (and contributors) will thank you. I wrote detailed README files, API docs, and setup guides.

5. Open Source Isn’t Just About Code

It’s about community, communication, and collaboration.

6. Learn by Building Real Projects

Tutorials taught me syntax. This project taught me engineering.

7. It’s Okay to Not Know Everything

I Googled constantly. I read documentation. I asked for help. That’s normal.


🛠️ Tools That Made Me 10x More Productive

  1. GitHub Copilot - Autocompleting boilerplate saved hours
  2. Supabase Studio - Visual database management was a game-changer
  3. Figma - Designing UI before coding prevented rework
  4. Postman - Testing API endpoints in isolation
  5. Chrome DevTools - React DevTools and Network tab were essential
  6. Vercel - One-click deployments = faster iteration
  7. Linear - Task management kept me organized

💡 Resources That Helped Me

Documentation I Lived In:

Courses/Videos:

  • “Just Ship It” mindset from Pieter Levels
  • React patterns from Kent C. Dodds
  • Database design from Hussein Nasser’s YouTube

Communities:

  • Reddit: r/webdev, r/reactjs
  • Discord: Supabase Community
  • Twitter: #buildinpublic

🎬 Conclusion: The Journey Continues

Building this Class Dues Tracker taught me more than any course could:

  • Technical skills: Full-stack development, database design, security
  • Product thinking: User research, MVP, iteration
  • Soft skills: Communication, feedback handling, scope management
  • Real-world impact: Solving problems for real users

But the most important lesson? You don’t need to be an expert to build something useful.

I wasn’t a senior developer when I started this. I was just someone frustrated with a problem, willing to learn, and determined to ship.

If you’re reading this and thinking “I could never build something like this” — yes, you can. Start small. Break problems into pieces. Google relentlessly. And most importantly, just start.


📞 Let’s Connect

I’d love to hear your thoughts, questions, or your own project stories!

- Email: wishotstudio@gmail.com

🙏 Thank You

To everyone who:

  • Beta tested the app
  • Gave feedback (even the harsh stuff)
  • Encouraged me when I wanted to quit
  • Shared this project

And to you, reading this — thanks for making it to the end. Now go build something awesome! 🚀


What’s your biggest challenge in your current project? Drop a comment below — I’d love to help!


This post is part of my #buildinpublic journey. Follow along as I continue iterating on this project and building new ones!

Top comments (1)

Collapse
 
nwaba_jceze_b93e2bffdaa8 profile image
Nwaba JC Eze

You're doing really great, Wizzy