The web system behind KKU's 2026 freshmen welcome — registration, e-tickets, check-in, points and a staff back office, built for roughly 8,500 freshmen and 500 staff.




The web system behind Khon Kaen University's KKU63 freshmen welcome — one app covering event registration, e-tickets, on-site check-in, activity points, friend grouping, and the dashboards the organising team actually ran the week on. The planning figures are written into the repo's own docs: roughly 8,500 freshmen and 500 staff.
First commit 11 May 2026, last commit 2 August 2026 — 364 commits over three months. The web app is 934 TypeScript files, about 124,000 lines, with 97 test files. Alongside it sits a console folder of Jupyter notebooks for back-office data work, such as pulling the roster of 3,138 incoming students across 148 programmes.
Separated by permission rather than by deployment — around 65 pages in total.
Every permission hangs off Firebase Auth custom claims — roleGroup, roleType, volunteerRole, friendshipGroup, isOnboarded, isVerified — and every page declares its own AccessRequirement, which returns the reason it refused rather than just a yes or no.
kku-63.vercel.app, signed into mostly through the university's own OAuth8,500 and 500 are not decoration in the docs — they are multipliers. A participant page that reads 20 documents becomes roughly 170,000 reads once every freshman opens it; a staff page that reads 500 becomes roughly 250,000 once the whole team does. Query shape is therefore treated as a correctness concern rather than something to tune at the end.
orderBy and limit, with cursor pagination wherever the data can growbyIds helpers, reusing one map across a request instead of reading the same rows twiceevent/{eventId}/counters/e-ticket, not from counting the event's tickets, with a test for concurrent check-inskku-location, event — sits behind a service-level cacheWriting a narrow query is an afternoon's work. Keeping it narrow is not something good intentions hold, because listServerUserPublic() and a filter afterwards is always the shorter thing to type. So the point was to make that not pass.
web/scripts/firestore-guard.mjs blocks listServerX() outside an allowlist
web/eslint.config.mjs server-only code cannot import client repositories
web/AGENTS.md the short mandatory rules, for AI agents
docs/firestore-query-knowledge.md the rationale, examples and decision tree
docs/firestore-read-guardrails.md the checklist used when reviewing a PRThe guard's allowlist names each path that is genuinely allowed to read a whole collection, with its reason beside it. Adding to it became a decision somebody has to defend rather than a line somebody types.
If the answer is “read all documents and filter in memory”, the default answer is no for participant and staff hot paths.
The PR checklist asks the same questions every time — if this collection grows 10x, is the query still bounded, and is the code reading these documents because the UI needs them or because there is no narrower repository method yet.
The site was permanently retired on 2 August 2026 once the week was over. main is locked behind maintenance mode and a separate demo branch exists to run it, carrying a bold note not to merge it back and not to expose it publicly until it points at a demo dataset — because this project is wired to a database holding real student records, drug allergies, food allergies and chronic conditions included.
Fourth year building this system, and the first with tests, with the reasoning behind its decisions written down, and with tooling that refuses the old mistakes on its own rather than waiting for somebody to remember them.