---
title: "Gradsy"
description: "A centralized platform for students, counselors, and admins with role-based access - built with Next.js and NestJS, and deployed on AWS using S3, SES, and Lightsail."
url: https://riteshkc.com.np/work/gradsy
source: https://riteshkc.com.np/work/gradsy.md
updated: 2026-09-03
site: "Ritesh KC"
---
# Gradsy

Turning a Manual Workflow into a Scalable Product

- Year: 2026
- Role: Full-stack engineer
- Duration: Handover Completed
- Stack: Next.js, NestJS, TypeScript, Prisma, PostgreSQL, Redis, BullMQ, AWS, AWS Simple Email Service, S3, Lightsail
- Live: https://dev.gradsy.io/

## Summary

A centralized platform for students, counselors, and admins with role-based access - built with Next.js and NestJS, and deployed on AWS using S3, SES, and Lightsail.

## Problem

Students struggled to find suitable universities and manage applications, while counselors and admins had fragmented workflows for tracking students, documents, and applications.

## Outcome

Gradsy brought university discovery, student applications, and consultancy workflows into one platform - simplifying the experience for students, counselors and admins.

From a consultancy website to a product built around the real problems students face

The project started with a simple request from [**UniHub**](https://unihubnetwork.com/), an education consultancy in Nepal - build a modern landing page for their business.

We built the website, but as we worked closely with the team, we started seeing a bigger problem.

Students, counselors, and admins were managing too much of the admission process manually. Information was scattered, communication was messy, and keeping track of students through different stages was difficult.

That became **Gradsy**.

![unihub landing page hero](https://cdn.riteshkc.com.np/media/unihub-landing.avif)

## From landing page to product

What started as a website turned into a platform designed around the actual workflow of an education consultancy.

I worked as the full stack developer, taking care of the frontend, backend, authentication, file management, emails, and deployment.

The goal wasn't to build another dashboard.

It was to make the day-to-day work easier for students, counselors, and admins.

![An admin dashboard with four counters, quick actions, and an activity feed naming the actor and their role for each recent change.](https://cdn.riteshkc.com.np/media/admin-dashboard.avif)

## Why Next.js

Gradsy had two very different sides - a public-facing experience and a highly interactive application.

**Next.js** gave me the right balance.

I could build fast, SEO-friendly pages for the public side while using the same application for the complex dashboard experience.

It also kept the frontend architecture straightforward as the product grew.

![A student profile with personal and contact fields redacted, beside a sidebar tracking completion across personal, academic and preferences sections.](https://cdn.riteshkc.com.np/media/student-profile.avif)

## Why NestJS

As the number of workflows increased, the backend needed structure.

I chose **NestJS** because it gave me a clean way to organize the application around modules, services, controllers, guards, and business logic.

This became especially useful when different users started having different roles and workflows.

Students needed one experience.

Counselors needed another.

Admins needed a much broader view.

The backend had to keep all of that organized.

![nestjs architecture](https://cdn.riteshkc.com.np/media/nestjs.avif)

## Authentication with Better Auth

Authentication wasn't something I wanted to build from scratch.

**Better Auth** gave us the foundation for secure authentication and session management while keeping the implementation flexible enough for Gradsy's different user roles.

That meant less time maintaining authentication code and more time solving the actual product problems.

![A sign-in screen with an email field, a Cloudflare Turnstile notice, a button to email a sign-in link, and a password fallback below.](https://cdn.riteshkc.com.np/media/student-signin.avif)

![The delivered sign-in email, stating that the link works once and expires in 24 hours.](https://cdn.riteshkc.com.np/media/student-magic-link-email.avif)

**Magic Link** signup using [Better Auth](https://better-auth.com/) and AWS Simple Email Service SES.

## AWS behind the scenes

The application needed reliable infrastructure without making the project unnecessarily complicated.

I used **Amazon S3** for storing documents and other uploaded files.

**Amazon SES** handled transactional emails and communication from the platform.

And **Lightsail** gave us a simple way to deploy and run the application while keeping infrastructure costs predictable.

![aws infra with lightsail SES](https://cdn.riteshkc.com.np/media/aws-infra.avif)

## What I built

Gradsy became a full workflow platform for the consultancy.

Students could manage their applications and information.

Counselors could manage students and their progress.

Admins could oversee the entire operation.

And behind all of that was a backend, authentication system, file storage, email infrastructure, and deployment setup designed to keep everything connected.

## The part I liked most

The interesting part of Gradsy wasn't choosing Next.js or NestJS.

It was watching a simple landing page turn into a real product.

We started by building what the client asked for.

Then we spent enough time understanding how they actually worked.

That changed what we built.

## Screens

### public

- A public universities directory with filters for country, degree level, tuition, institution type and intake, beside one result card. — The public side of the same data admins edit. Students browse before they ever create an account.
- A university profile page: photo gallery, a row of statistics, an about section, and a sidebar listing tuition, programmes and acceptance rate. — One university, assembled from the fields an admin fills in. Rank, cost and acceptance rate sit where a student compares them.

### student

- A sign-in screen with an email field, a Cloudflare Turnstile notice, a button to email a sign-in link, and a password fallback below. — Sign-in is a magic link by default, with Turnstile in front of it and a password path kept for anyone who wants one.
- The delivered sign-in email, stating that the link works once and expires in 24 hours. — The link is single-use and expires in 24 hours. Delivery goes out through SES on a queue rather than inline with the request.
- A student profile with personal and contact fields redacted, beside a sidebar tracking completion across personal, academic and preferences sections. — Personal details, redacted here. The sidebar tracks completion across three sections and reports a single percentage.
- A course preferences screen covering study level, fields of study, preferred programs, destination countries, budget range and intake. — Preferences are structured fields rather than free text: study level, subjects, countries, budget and intake.
- A three-step new-application wizard on step one, choosing destination country, degree level and university. — Applying is three steps with a running summary and a save-draft escape hatch. A student doing this once, under stress, never sees the whole form at once.
- A document library, six categories with four required, every file carrying a verified badge and a banner confirming the required set is complete. — Documents are uploaded once and reused across applications. The required set is a gate: until every one of them verifies, submission stays closed.
- One application's status history, seven entries deep, each state change stamped with a date and the counselor's written reason. — The student sees the same state machine the counselor drives, rejections included. Every transition carries the reason somebody typed.
- A student's articles list filtered by state, showing one article card marked under review. — Students write articles too. The same review states apply: draft, under review, published, changes requested, archived.
- A community feed with vote counts, tag filters, a pinned announcement from the Gradsy team, and a sidebar of tags and guidelines. — One feed, tagged by topic and voted on. The pinned post carries a team badge so official answers are distinguishable from replies.

### counselor

- A counselor's applications board with four columns - draft, pending review, sent to university, decided - each showing a count. — The counselor's queue is the pipeline's states, in order. Where an application has got to is the column it is sitting in.
- A delegated task queue with one open document check, filters by state, and claim, start, complete and escalate actions on the detail panel. — Tasks are claimed off a shared queue, so the claim is the operation that has to hold when two counselors click at once. Escalation hands one back.

### admin

- An admin dashboard with four counters, quick actions, and an activity feed naming the actor and their role for each recent change. — The activity feed names who did what, role badge attached. Most admin questions turn out to be questions about who last touched a record.
- A reports screen in light theme: applications by status, a country breakdown, top universities by application count, and an activity feed. — Reporting, in the light theme. The counts are from a staging database, not production traffic.
- An admin reviewing one application: verified documents, a warning that a missing statement of purpose blocks submission, and a status-change form. — Review is a status change plus a note, and the note is what the student reads. The missing-document rule blocks the student, not the reviewer.
- An admin delegation panel setting a counselor's delegation level, maximum concurrent tasks, and which categories of work they may take. — Delegation is per-permission and capacity-capped: a level, a task ceiling, and a checklist of what this counselor may do. Seniority gates the rest.
- The tasks tab of the same counselor record, listing an assigned task with its schedule and the comment thread between admin and counselor. — The same counselor record, tasks tab. Assignment, schedule and the thread live together so a handover has one place to look.
- A university editor with tabs for overview, academics, costs, media and programs, showing basic information, location and record metadata. — Universities are edited here and rendered publicly from the same record. The slug is visible because it is a URL somebody may already have shared.
- An article review queue filtered to under review, with author-type filters and approve, reject and preview actions on the card. — Articles from students and counselors land in one queue. Approve, request changes, or preview it as published.
- A crop dialog over the article editor, with a zoom slider, rotate controls, a rule-of-thirds grid and a live preview of the cover. — Cover images are cropped in the browser before upload, so the stored file is already the shape the card needs.

## Case study

What the student sees, what the office sees, from one set of records: https://riteshkc.com.np/case-studies/gradsy (markdown: https://riteshkc.com.np/case-studies/gradsy.md)

## Related writing

- [A status machine belongs in a table](https://riteshkc.com.np/blog/a-status-machine-belongs-in-a-table): Nine statuses, two sets of allowed transitions, and two names for every state. Moving all of that into rows instead of an if chain got engineering out of the workflow business.
- [Endpoints that refuse to be oracles](https://riteshkc.com.np/blog/endpoints-that-refuse-to-be-oracles): A 404 on unsubscribe tells an attacker which tokens are real. A 409 on subscribe tells them who is on your list. Honest status codes leak, and the fix reads like a bug.
- [Every reset link returned 400](https://riteshkc.com.np/blog/every-reset-link-returned-400): The reset endpoint was fine. The token was fine. The email was fine. The bug was one path in a captcha config, and the word doing the damage was "includes".
- [Ready to submit is not a boolean](https://riteshkc.com.np/blog/ready-to-submit-is-not-a-boolean): A submit gate that looked like one rule and needed three, plus an error message that names which of four things the student actually has to fix.
- [Six socket events, and not one refetch](https://riteshkc.com.np/blog/six-socket-events-and-not-one-refetch): Realtime usually arrives as a second copy of your state. Treating every socket event as a write into the query cache instead of a signal to refetch keeps it to one.
- [A trust score you can rebuild from the log](https://riteshkc.com.np/blog/a-score-you-can-rebuild-from-the-log): Ten lines that replay a moderation history into a score, and why clamping inside the fold instead of after it is the difference between working and quietly drifting.
- [Cache invalidation when the cache is in another repo](https://riteshkc.com.np/blog/cache-invalidation-in-another-repo): An admin edits a university and the public page keeps the old number for a day. The backend now calls the frontend, over tag names nothing checks.
- [Overbooking is a database problem](https://riteshkc.com.np/blog/overbooking-is-a-database-problem): Counting rows before you insert one is not a capacity check. Here is how a unique index, a constraint violation, and a NULL ended up doing the work instead.
- [Rate limiting a provider without a rate limiter](https://riteshkc.com.np/blog/rate-limiting-by-queue-shape): SES caps sends per second. The fix was worker concurrency of one and a sleep, plus an honest note in the code about the case where it stops working.
- [The fast moderation layer does less on purpose](https://riteshkc.com.np/blog/the-fast-moderation-layer-does-less): A synchronous gate on five routes that blocks almost nothing, and an async worker that decides everything else. Splitting them by latency was the wrong axis.
