sakibkx
Start a project

Delegate operations platform for an international conference

University representatives and government officials fly into China for one conference. Every one needs a visa invitation letter, a hotel, a transfer and a badge, tracked across five desks at once.

RoleBuilt the backend end to end. Led the frontend.
Written by me93% of the backend
Year2026
StatusLive
Visit expo.malishaedu.com
expo.malishaedu.com
The conference website and the delegate portal sign-in page
Measured
API endpoints281@Get/@Post/@Patch/@Put/@Delete decorators counted across 24 controllers
Data model50 tablesmodel declarations in a 1,916-line Prisma schema, plus 26 enums
Migrations38migration directories, 3,082 lines of hand-written SQL, applied to a live database
Doc coverage83%234 ApiOperation annotations against 281 endpoints
TODO markers0grep for TODO/FIXME/HACK across 30,039 lines, cross-checked against all 130 commits

How authorship was measured. 27,834 of 30,040 surviving TypeScript lines in src/, measured with per-file git blame at commit 2fa1120, excluding lockfiles and generated code. 95.4% of commits are mine.

The problem

An international education conference in China brings in university representatives, government officials and education partners from dozens of countries. Every one of them needs a formal invitation letter before they can apply for a Chinese visa. A letter that arrives late, or with a misspelled passport name, means that person does not get on the plane.

Once they land, five different desks touch the same person: invitation, travel, hotel, venue and programme. All five update the same record at the same time, and the organiser needs to know where any delegate is at any moment.

It was being run on spreadsheets.

What I built

I designed and built the backend end to end, and led the frontend where I own the data layer, the permission engine, the admin portal and the QR subsystem.

The invitation letter is generated on demand and never written to disk. An admin edits a Markdown template per programme. The renderer distinguishes three kinds of token, so a value can be injected safely inside an HTML attribute as well as in a table body. It renders to A4 through headless Chromium, waits for document.fonts.ready so the web fonts actually apply, and embeds a per-guest QR code as a data URI. One browser instance is kept alive for the process lifetime. If Chromium crashes, the cached promise is dropped so the next render self-heals, and a failed launch clears the cache too, because otherwise every later render reuses the rejected promise and PDF generation stays broken until someone restarts the process.

Access control is two orthogonal layers. One controls which module a staff member works in. The other controls which of nine QR status transitions they may perform. Most systems collapse these into a single role field and then cannot express the thing the client actually needed: a hotel staffer who may scan check-ins but not checkouts. The nine flags are stored as nine columns and mapped to a nested object in exactly one file.

Authentication has a real revocation path. Refresh tokens are opaque random strings stored only as hashes, never as tokens that can be read. Replaying an already-rotated token revokes every outstanding token for that user. Logout verifies the signature before blacklisting, so a forged token cannot be used to flood the blacklist table.

Passport data is read by Gemini against a generated JSON schema, with five distinct failure boundaries that all degrade to asking the person to type it in.

The part that was hardest

Thirty-eight migrations applied to a live database. Converting a free-text country field into a 250-value enum through a CASE alias map. Collapsing four separate status enums, which meant dropping and recreating column defaults around each type swap. Unpacking a Postgres enum array into boolean columns.

Human-readable delegate codes had to be race-free, so the sequence reset and the next value are taken in one SQL statement. A seeded database cannot hand back a code that is already in use.

Stack
  • NestJS 11
  • TypeScript
  • PostgreSQL 16
  • Prisma 7
  • Next.js 16
  • Puppeteer
  • Google Gemini