I'm a CSE student at Rajshahi University of Engineering & Technology (RUET).
Every semester the same conversation happens in every batch: what is that
teacher actually like? The answer travels by word of mouth, unevenly, and
juniors mostly get nothing.
So I built RUET Faculty Review —
anonymous student reviews of teaching, closed to RUET email addresses.
Next.js, Prisma, Postgres, Vercel. None of that was the hard part.
Three things were.
1. A moderation queue nobody reads is a graveyard
The obvious design: review comes in, goes to a queue, admin approves it. I
built it, then imagined it two weeks into exams and realised I would never
open it. Students' writing would just sit there.
So the AI decides everything — Groq, JSON Schema mode, four verdicts.
PENDING now means exactly one thing: the AI could not be reached. A
cron retries those, and I get one alert, ever, if moderation has been down
for hours.
My first prompt was also too strict. It rejected a review saying a teacher
explains too fast and skips questions — exactly the honest criticism the site
exists to collect. The rewrite leads with DEFAULT TO SAFE: harsh, angry,
informal criticism of teaching is fine. Only personal attacks, accusations
and spam are not. An over-strict filter empties the site.
2. AVG(rating) is not one student, one vote
Five students review a teacher. One wrote eight reviews — they took eight of
that teacher's courses — all 1 star. Four others wrote one each, all 3 stars.
flat average: (1×8 + 3×4) / 12 = 1.67
One person outvoted four, using entirely legitimate reviews.
The fix is a mean of means — average each student first, then average those:
one student, one vote: (1 + 3 + 3 + 3 + 3) / 5 = 2.6
Per-course averages on a teacher's page still use the plain average,
deliberately. There the question is "what is this course like", and each
review is one answer to it.
3. A server action is an HTTP endpoint, whether you think of it as one or not
I had a helper — teacherCapProblem(userId, teacherId) — exported from a
'use server' file because a page needed it. It answers "has user X
reviewed teacher Y, and how often". The page it served was behind a login.
The helper was not.
On a site whose entire promise is that nobody learns who wrote what, that is
the one question it must never answer.
The fix wasn't to add an auth check. It was to move it to a plain module, so
the endpoint stops existing. A helper that takes a userId should never be
exported from a 'use server' file. There's now a test that fails if any
exported action lacks a session check.
Three lessons, if you're building something similar:
- Write the honest version of your constraint first. "I'll check the queue daily" was a lie I told myself, and a week of work sat on top of it.
-
Print the artefact, don't trust the generator. My
robots.txtgenerator emittedAllow: /, which cancelledDisallow: /and opened every teacher page to crawling. Reading the generated file caught it; reading the code had not. - Test the failure path. A health check that returns 200 while the database is down is worse than none, because now you trust it.
The site is closed to RUET students, which is the point — it isn't affiliated
with the university. If you've built something similar, I'd like to hear how
you handled moderation. That's the part I'm still least sure about.
Top comments (1)
Official Platform Update
Security protocols have been updated for all developer accounts.