---
title: "Gradsy"
description: "Der gesamte Betrieb einer Auslandsstudien-Beratung über zwei Repos. Studierende bewerben sich, Berater bewegen Bewerbungen durch Zustände, Admins machen den Rest. Viel davon war Nebenläufigkeit."
url: https://riteshkc.com.np/de/work/gradsy
source: https://riteshkc.com.np/de/work/gradsy.md
updated: 2026-09-03
site: "Ritesh KC"
---
# Gradsy

Tabellen und E-Mails einer Auslandsstudien-Beratung durch eine Plattform ersetzt

- Year: 2026
- Role: Full-Stack-Entwickler
- Duration: laufend
- Stack: Next.js, React, NestJS, TypeScript, Prisma, PostgreSQL, Redis, BullMQ, AWS
- Live: https://dev.gradsy.io/

## Summary

Der gesamte Betrieb einer Auslandsstudien-Beratung über zwei Repos. Studierende bewerben sich, Berater bewegen Bewerbungen durch Zustände, Admins machen den Rest. Viel davon war Nebenläufigkeit.

## Problem

Eine Beratung wickelte Bewerbungen über Tabellen und E-Mail ab. Drei Nutzertypen brauchten unterschiedliche Sichten auf dieselbe Strecke, und ein Entwickler musste das alles bauen.

## Outcome

Postgres-Constraints und Queue-Jobs halten die Regeln: ein Platz kann nicht überbucht werden, ein Job kann nicht doppelt zählen, und ein Cache leert sich über eine Prozessgrenze hinweg.

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

- Ein öffentliches Universitätsverzeichnis mit Filtern für Land, Abschlussniveau, Studiengebühren, Hochschultyp und Studienbeginn, daneben eine Ergebniskarte. — Die öffentliche Seite derselben Daten, die Admins bearbeiten. Studierende stöbern, bevor sie überhaupt ein Konto anlegen.
- Ein Universitätsprofil: Fotogalerie, eine Reihe Kennzahlen, ein Über-uns-Abschnitt und eine Seitenleiste mit Gebühren, Studiengängen und Zulassungsquote. — Eine Universität, zusammengesetzt aus den Feldern, die ein Admin pflegt. Rang, Kosten und Zulassungsquote stehen dort, wo Studierende sie vergleichen.

### student

- Ein Anmelde-Screen mit E-Mail-Feld, einem Cloudflare-Turnstile-Hinweis, einem Button für den Anmeldelink per E-Mail und einer Passwort-Alternative darunter. — Anmeldung standardmäßig per Magic Link, mit Turnstile davor und einem Passwort-Weg für alle, die ihn wollen.
- Die zugestellte Anmelde-E-Mail mit dem Hinweis, dass der Link einmal funktioniert und nach 24 Stunden verfällt. — Der Link ist einmal gültig und verfällt nach 24 Stunden. Der Versand läuft über SES in einer Queue statt inline mit dem Request.
- Ein Studierendenprofil mit geschwärzten Kontaktdaten, daneben eine Seitenleiste, die den Fortschritt über Persönliches, Akademisches und Präferenzen verfolgt. — Persönliche Angaben, hier geschwärzt. Die Seitenleiste verfolgt den Fortschritt über drei Abschnitte und meldet einen einzigen Prozentwert.
- Ein Screen für Studienpräferenzen: Abschlussniveau, Fachrichtungen, bevorzugte Programme, Zielländer, Budgetrahmen und Studienbeginn. — Präferenzen sind strukturierte Felder statt Freitext: Niveau, Fächer, Länder, Budget und Studienbeginn.
- Ein dreistufiger Assistent für eine neue Bewerbung, auf Schritt eins: Zielland, Abschlussniveau und Universität wählen. — Bewerben sind drei Schritte mit laufender Zusammenfassung und einem Entwurf als Notausgang. Wer das einmal und unter Druck macht, sieht nie das ganze Formular auf einmal.
- Eine Dokumentenablage mit sechs Kategorien, vier davon Pflicht, jede Datei mit Prüfsiegel und einem Banner, das den vollständigen Pflichtsatz bestätigt. — Dokumente werden einmal hochgeladen und über Bewerbungen hinweg wiederverwendet. Der Pflichtsatz ist ein Tor: Bis jedes einzelne geprüft ist, bleibt das Absenden zu.
- Der Statusverlauf einer Bewerbung, sieben Einträge tief, jede Zustandsänderung mit Datum und der schriftlichen Begründung des Beraters. — Studierende sehen dieselbe Zustandsmaschine, die der Berater bedient — Ablehnungen eingeschlossen. Jeder Übergang trägt die Begründung, die jemand getippt hat.
- Die nach Zustand gefilterte Artikelliste einer studierenden Person, mit einer Artikelkarte im Zustand „in Prüfung“. — Studierende schreiben auch Artikel. Es gelten dieselben Prüfzustände: Entwurf, in Prüfung, veröffentlicht, Änderungen erbeten, archiviert.
- Ein Community-Feed mit Stimmenzahlen, Tag-Filtern, einer angehefteten Ankündigung des Gradsy-Teams und einer Seitenleiste mit Tags und Regeln. — Ein Feed, nach Themen getaggt und bewertet. Der angeheftete Beitrag trägt ein Team-Abzeichen, damit offizielle Antworten von Antworten unterscheidbar sind.

### counselor

- Das Bewerbungs-Board eines Beraters mit vier Spalten — Entwurf, zur Prüfung, an Universität gesendet, entschieden — jede mit Anzahl. — Die Warteschlange des Beraters ist die Zustandsfolge der Strecke, in Reihenfolge. Wo eine Bewerbung steht, ist die Spalte, in der sie liegt.
- Eine delegierte Aufgaben-Warteschlange mit offener Dokumentenprüfung, Filtern nach Zustand und den Aktionen Übernehmen, Starten, Abschließen und Eskalieren. — Aufgaben werden aus einer gemeinsamen Queue übernommen — die Übernahme ist also die Operation, die halten muss, wenn zwei Berater gleichzeitig klicken. Eskalation gibt eine zurück.

### admin

- Ein Admin-Dashboard mit vier Zählern, Schnellaktionen und einem Aktivitätsfeed, der zu jeder Änderung die handelnde Person und ihre Rolle nennt. — Der Aktivitätsfeed nennt, wer was getan hat, mit Rollen-Abzeichen. Die meisten Admin-Fragen sind am Ende Fragen danach, wer einen Datensatz zuletzt angefasst hat.
- Ein Auswertungs-Screen im hellen Theme: Bewerbungen nach Status, eine Länderaufschlüsselung, die häufigsten Universitäten und ein Aktivitätsfeed. — Auswertungen, im hellen Theme. Die Zahlen stammen aus einer Staging-Datenbank, nicht aus echtem Traffic.
- Ein Admin prüft eine Bewerbung: geprüfte Dokumente, ein Hinweis auf ein fehlendes Motivationsschreiben, das das Absenden blockiert, und ein Statusformular. — Prüfung ist eine Statusänderung plus Notiz, und die Notiz ist das, was der Studierende liest. Die Regel für fehlende Dokumente blockiert den Studierenden, nicht den Prüfer.
- Ein Delegations-Bereich, in dem ein Admin Delegationsstufe, maximale gleichzeitige Aufgaben und die erlaubten Aufgabenkategorien eines Beraters festlegt. — Delegation läuft pro Berechtigung und mit Kapazitätsgrenze: eine Stufe, eine Aufgaben-Obergrenze und eine Liste dessen, was dieser Berater darf. Den Rest regelt Seniorität.
- Der Aufgaben-Tab desselben Berater-Datensatzes mit einer zugewiesenen Aufgabe samt Zeitplan und dem Kommentarverlauf zwischen Admin und Berater. — Derselbe Berater-Datensatz, Aufgaben-Tab. Zuweisung, Zeitplan und Verlauf liegen zusammen, damit eine Übergabe nur einen Ort zum Nachsehen hat.
- Ein Universitäts-Editor mit Tabs für Überblick, Studium, Kosten, Medien und Programme, hier mit Basisangaben, Standort und Metadaten des Datensatzes. — Universitäten werden hier bearbeitet und aus demselben Datensatz öffentlich ausgespielt. Der Slug ist sichtbar, weil er eine URL ist, die jemand schon geteilt haben kann.
- Eine Artikel-Prüfliste, gefiltert auf „in Prüfung“, mit Filtern nach Autorentyp und den Aktionen Freigeben, Ablehnen und Vorschau auf der Karte. — Artikel von Studierenden und Beratern landen in einer Warteschlange. Freigeben, Änderungen erbitten oder als veröffentlicht ansehen.
- Ein Zuschneide-Dialog über dem Artikel-Editor, mit Zoom-Regler, Drehsteuerung, Drittel-Raster und einer Live-Vorschau des Titelbilds. — Titelbilder werden im Browser zugeschnitten, bevor sie hochgeladen werden — die gespeicherte Datei hat also schon die Form, die die Karte braucht.

## Case study

Was der Studierende sieht, was das Büro sieht — aus einem Datenbestand: https://riteshkc.com.np/de/case-studies/gradsy (markdown: https://riteshkc.com.np/de/case-studies/gradsy.md)

## Related writing

- [Eine Statusmaschine gehört in eine Tabelle](https://riteshkc.com.np/de/blog/a-status-machine-belongs-in-a-table): Neun Status, zwei Sätze erlaubter Übergänge und zwei Namen für jeden Zustand. Das alles in Zeilen statt in eine if-Kette zu verlegen, hat die Entwicklung aus dem Workflow-Geschäft geholt.
- [Endpoints, die sich weigern, Orakel zu sein](https://riteshkc.com.np/de/blog/endpoints-that-refuse-to-be-oracles): Ein 404 beim Abmelden verrät einem Angreifer, welche Tokens echt sind. Ein 409 beim Anmelden verrät, wer auf der Liste steht. Ehrliche Statuscodes lecken, und die Lösung liest sich wie ein Bug.
- [Jeder Reset-Link lieferte 400](https://riteshkc.com.np/de/blog/every-reset-link-returned-400): Der Endpoint war in Ordnung. Das Token war in Ordnung. Die E-Mail war in Ordnung. Der Fehler lag in einem Pfad einer Captcha-Konfiguration, und das schädliche Wort hieß „includes“.
- [„Bereit zum Absenden“ ist kein Boolean](https://riteshkc.com.np/de/blog/ready-to-submit-is-not-a-boolean): Ein Absende-Gate, das nach einer Regel aussah und drei brauchte, dazu eine Fehlermeldung, die benennt, welche von vier Sachen der Studierende tatsächlich beheben muss.
- [Sechs Socket-Events, und kein einziges Refetch](https://riteshkc.com.np/de/blog/six-socket-events-and-not-one-refetch): Realtime kommt meist als zweite Kopie Ihres Zustands. Jedes Socket-Event als Schreibvorgang in den Query-Cache zu behandeln statt als Signal zum Nachladen hält es bei einer.
- [Ein Trust Score, den man aus dem Log neu bauen kann](https://riteshkc.com.np/de/blog/a-score-you-can-rebuild-from-the-log): Zehn Zeilen, die eine Moderationshistorie zu einem Score abspielen — und warum das Begrenzen innerhalb des Folds statt danach der Unterschied zwischen funktionieren und leise driften ist.
- [Cache-Invalidierung, wenn der Cache in einem anderen Repo liegt](https://riteshkc.com.np/de/blog/cache-invalidation-in-another-repo): Ein Admin bearbeitet eine Universität, und die öffentliche Seite behält einen Tag lang die alte Zahl. Das Backend ruft jetzt das Frontend auf — über Tag-Namen, die niemand prüft.
- [Überbuchung ist ein Datenbankproblem](https://riteshkc.com.np/de/blog/overbooking-is-a-database-problem): Zeilen zu zählen, bevor man eine einfügt, ist keine Kapazitätsprüfung. Wie stattdessen ein Unique Index, eine Constraint-Verletzung und ein NULL die Arbeit übernommen haben.
- [Einen Anbieter drosseln, ohne Rate Limiter](https://riteshkc.com.np/de/blog/rate-limiting-by-queue-shape): SES begrenzt Sendungen pro Sekunde. Die Lösung war Worker-Nebenläufigkeit von eins plus ein Sleep — und eine ehrliche Notiz im Code über den Fall, in dem das nicht mehr trägt.
- [Die schnelle Moderationsschicht tut absichtlich weniger](https://riteshkc.com.np/de/blog/the-fast-moderation-layer-does-less): Ein synchrones Gate auf fünf Routen, das fast nichts blockiert, und ein asynchroner Worker, der alles andere entscheidet. Sie nach Latenz zu trennen war die falsche Achse.
