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