karthik.dev
Back to blogSaaS

Building CodeMyFYP Academy: Multi-Tenant EdTech SaaS from Zero to AICTE

2026-06-10 · 5 min read

The Problem: Fragmented University Tech Stack

Universities affiliated with VTU, Bangalore University, and Mysore University manage student internships, course certifications, and industry connections through a patchwork of email, spreadsheets, and disconnected Google Forms. There's no single platform where students apply, mentors teach, recruiters hire, and administrators oversee — and no automated pipeline connecting them.

CodeMyFYP Academy is that platform.

The Five User Roles

The most important architectural decision was the role system. Getting this wrong would mean duplicating logic across roles or creating unmaintainable permission tangles.

Role Primary Capabilities
Student Enroll in courses, apply for internships, take assessments, receive certificates
Mentor Create courses, grade assignments, manage student progress
Recruiter Post vacancies, review student profiles, manage applications
Faculty Oversee student enrollment, view academic performance
Administrator Full platform management — users, billing, content, analytics

RBAC is enforced at the API layer, not just the UI. A student session cannot access recruiter endpoints even with a crafted token:

// src/middleware/rbac.ts
export function requireRole(...roles: UserRole[]) {
  return async (req: NextRequest, res: NextResponse) => {
    const session = await getSession(req);
    
    if (!session || !roles.includes(session.role)) {
      return NextResponse.json(
        { error: "Insufficient permissions" },
        { status: 403 },
      );
    }
  };
}

// Usage:
// GET /api/admin/analytics → requireRole("administrator")
// POST /api/courses → requireRole("mentor", "administrator")
// GET /api/vacancies → requireRole("recruiter", "administrator")

Database Architecture: PostgreSQL + MongoDB

Most data is relational (users, roles, enrollments, applications) — PostgreSQL handles this. Course content is document-structured (rich text, embedded videos, quiz questions with nested options) — MongoDB handles this.

// PostgreSQL: structured system-of-record data
// - users, roles, permissions
// - enrollments and progress tracking
// - payments and subscriptions
// - internship applications

// MongoDB: flexible content data
// - course lessons (rich text, media URLs, ordering)
// - quiz questions (nested options, explanations)
// - certificates (template, metadata, PDF path)
// - notifications (polymorphic structure)

Mixing databases adds operational complexity. It was the right trade-off because forcing course content into a relational schema would have required multiple JOINs for every course render.

PhonePe Payment Integration

PhonePe is the dominant payment gateway for Indian ed-tech. The integration has two critical requirements: idempotency and webhook reliability.

Idempotency: if a payment request is retried (network timeout, user double-click), we must not charge twice.

// src/services/payment.ts
async function initiatePayment(order: Order): Promise<PaymentResponse> {
  // Idempotency key: order ID + attempt number
  const idempotencyKey = `${order.id}:${order.paymentAttempts}`;
  
  // Check if we already initiated this payment
  const existing = await db.payment.findUnique({
    where: { idempotencyKey },
  });
  if (existing) return { payUrl: existing.paymentUrl };
  
  const phonePePayload = {
    merchantId: process.env.PHONEPE_MERCHANT_ID,
    merchantTransactionId: idempotencyKey,
    amount: order.amountPaise,  // PhonePe uses paise, not rupees
    redirectUrl: `${process.env.BASE_URL}/payment/success`,
    callbackUrl: `${process.env.BASE_URL}/api/webhooks/phonepe`,
  };
  
  const response = await phonePeAPI.initiatePayment(phonePePayload);
  
  // Store payment record before redirecting
  await db.payment.create({
    data: {
      orderId: order.id,
      idempotencyKey,
      paymentUrl: response.data.instrumentResponse.redirectInfo.url,
      status: "INITIATED",
    },
  });
  
  return { payUrl: response.data.instrumentResponse.redirectInfo.url };
}

Webhook reliability: PhonePe sends payment status via webhook. The webhook handler must be idempotent — PhonePe may retry the same event multiple times.

// src/app/api/webhooks/phonepe/route.ts
export async function POST(req: Request) {
  const payload = await req.json();
  
  // Verify PhonePe signature
  const isValid = verifyPhonePeSignature(payload, req.headers.get("x-verify")!);
  if (!isValid) return Response.json({ error: "Invalid signature" }, { status: 401 });
  
  const transactionId = payload.data.merchantTransactionId;
  
  // Idempotency: check if we already processed this event
  const alreadyProcessed = await db.webhookEvent.findUnique({
    where: { transactionId },
  });
  if (alreadyProcessed) return Response.json({ status: "already_processed" });
  
  // Process in a transaction
  await db.$transaction(async (tx) => {
    await tx.webhookEvent.create({ data: { transactionId, payload } });
    await tx.payment.update({
      where: { idempotencyKey: transactionId },
      data: { status: payload.code === "PAYMENT_SUCCESS" ? "SUCCESS" : "FAILED" },
    });
    
    if (payload.code === "PAYMENT_SUCCESS") {
      await enrollStudent(tx, transactionId);
    }
  });
  
  return Response.json({ status: "processed" });
}

ISR for Course Content Performance

Course pages are read-heavy and rarely change. Next.js ISR (Incremental Static Regeneration) with a 1-hour revalidation period dramatically reduced database load:

// src/app/courses/[slug]/page.tsx
export const revalidate = 3600; // revalidate every hour

export default async function CoursePage({ params }: { params: { slug: string } }) {
  const course = await getCourseContent(params.slug); // cached at build/revalidation time
  return <CourseRenderer course={course} />;
}

AICTE National Internship Portal

The platform is officially listed on the AICTE National Internship Portal — validating that CodeMyFYP Academy meets government standards for student internship platforms. This required:

  • A documented internship application and tracking workflow
  • Certificate generation with verifiable credentials
  • Mentor/organization verification process
  • Student outcome reporting

Results

  • Automated onboarding — students go from registration to enrolled in under 5 minutes
  • Certificate generation — fully automated, verifiable PDFs on course completion
  • Five-role RBAC — zero permission escalation vulnerabilities across 30+ API routes
  • PhonePe payments — idempotent, webhook-confirmed, zero double-charge incidents

The platform currently serves students across institutions affiliated with VTU, Bangalore University, and Mysore University.

Live: academy.codemyfyp.com