Avanish JhaStart a project

WorkFifty Villagers

Fifty Villagers

A platform for an education NGO: applications, admit cards, results and donations, with an admin panel the team runs day to day.

Role
Solo full stack
Stack
React, Node.js, Postgres, Docker
Status
Live
Years
2025 to now
Updates shipped automatically
46
Schema migrations
17
The Fifty Villagers home page on desktop: a large Hindi headline over a blurred library, a "Donate now" button, and a photo of a teacher pointing at a whiteboard.
The home page, October 2026

Fifty Villagers runs an entrance test for students. The team needed one place for the whole cycle: applications, admit cards with exam centres, results and donations, with an admin side they could run themselves.

The brief: admin and student accounts, Razorpay payments that are verified before they count, and a record of every change an admin makes.

  • Refresh tokens stored hashed

    Refresh tokens are hashed before they are saved, like passwords, so a copy of the database cannot be used to sign in. Logging out revokes them.

  • Signature-verified Razorpay webhooks

    Every payment event is verified server-side before it touches the records. The handler is idempotent, so a payment that is already recorded is never recorded twice.

  • A 17-migration Postgres schema with an activity log

    An activity log of every admin action: who did it, what changed and when. No silent edits.

  • A small VPS instead of managed hosting

    Docker, Nginx and HTTPS on a small VPS, deployed by GitHub Actions with a health check after every release.

razorpay-webhook.js
import crypto from 'crypto';

// Razorpay signs every event with our shared secret.
// Verify before touching the ledger; replays are dropped.
export function verifyWebhook(rawBody, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  );
}

Note to self

Keep: treating every payment as a small state machine with tests around it. A payment can only take the steps it's allowed to, and once it's paid it never goes back to unpaid, so a retried webhook or a double tap can't leave it in a strange state.

Change: the activity log only arrived at the sixteenth migration. I'd build it in from the first, and turn on Postgres row-level security from the start instead of relying only on application-level RBAC. Two layers are cheap; one is a foot-gun.

Next stop

Enterprise platform at CMS Computers →Maintenance, incidents and contracts

Let's build yours.

A few lines is enough. I'll reply with questions or a time to talk.