OneShiksha started as a side project. I was building tools for a relative who ran a small school in Satara and was drowning in paperwork. The first version was a PHP script that took attendance and sent SMS. It had a form, a MySQL table, and a cron job. It was ugly, but the school loved it because it saved them 10 hours a week.
Two years later, that PHP script has become a full-stack Next.js application serving 500+ schools, handling 8.4 lakh students, and processing lakhs of rupees in fee payments every month. Here is what the stack looks like now, and why it evolved the way it did.
Stack Choices: What I Picked and Why
The frontend and API layer run on Next.js 15 with the App Router. I chose it because it gives me server-side rendering (critical for SEO), server actions (no separate API routes for simple mutations), and a single codebase for dashboard, public website, and API.
The database is PostgreSQL via Supabase. The ORM is Prisma. I wasted months in the early days debugging raw SQL queries for multi-tenant data isolation. Prisma's type safety catches schema errors at compile time — a massive productivity gain when you're the only person writing code.
Authentication is handled by Clerk (migration to Better Auth is in progress). I chose Clerk initially because it handled org-based multi-tenancy out of the box — school admins, teachers, students, and parents all under one organizational umbrella with role-scoped access.
- Next.js 15 App Router for SSR, server actions, and unified codebase
- PostgreSQL + Prisma for type-safe multi-tenant queries
- Clerk for org-based auth with role-scoped access control
- Cloudinary for image/video storage; Uploadthing for file uploads
Multi-Tenancy: The Hardest Problem I Solved
The biggest technical challenge was data isolation. School A should never see School B's data — not through an API bug, not through a misconfigured query, not ever.
Every table in the database has an organization_id column. Every Prisma query includes a where: { organization_id: currentOrgId } clause. I built a helper function early on that automatically appends the org filter to every query — forgetting it once would be a data leak.
For the public website (this blog, pricing page, features), data is either static or fetched from separate non-tenant tables. The dashboard routes are all behind middleware that verifies org membership and role before rendering a single component.
- Every query scoped by organization_id — enforced at the helper function level
- Public website and dashboard are architecturally separated
- Role-based access control (Admin/Teacher/Student/Parent) scoped within each org
- No cross-org data can leak even through a misconfigured API call
The Things That Surprised Me
Two things I didn't expect when I started:
First, the notification system (SMS + WhatsApp + Email + Push) turned out to be more complex than the core school management features. Each channel has different rate limits, templates, delivery guarantees, and cost structures. WhatsApp Business API requires template approval. SMS gateways have different reliability in different states. I ended up building a notification engine with retry logic, channel fallback, and delivery tracking.
Second, Indian schools have wildly different workflows. A CBSE school in Mumbai runs differently from a state board school in rural Maharashtra from a coaching center in Kota. Building for "Indian schools" means building flexible enough to handle all of them, which often means more configuration options than I'd like.
Conclusion
OneShiksha is still a work in progress. There are features I want to build, performance bottlenecks I want to fix, and code I want to rewrite. But the platform handles millions of requests a month, stays above 99.9% uptime, and — most importantly — actually helps schools run better. If you're curious about any specific technical decision, I've tried to document it in our GitHub discussions. Or just reach out — I'm always happy to talk school software architecture.