Delegate operations for an international conference
Hundreds of university representatives and government officials fly into China for one conference. Every one of them needs a formal invitation letter to get a visa. If the letter is late or wrong, they cannot enter the country.
How authorship was measured. Lines surviving in the current tree, measured with per-file git blame across all tracked source, excluding lockfiles and generated code. 95.4% of commits are mine.
The problem
An international education conference in China brings in hundreds of 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 with no shadow copy. 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.
- NestJS 11
- TypeScript
- PostgreSQL 16
- Prisma 7
- Next.js 16
- Puppeteer
- Google Gemini
What this project does not prove
- This system has no published usage figures. I can tell you how large it is, not how many people used it.
- It has no automated tests. I verified it in a browser and I would not do that again.
- All delegate documents sit on one server's disk. That was a deliberate trade-off for the deadline, not an architecture for scale.