If you've ever applied to a university in Pakistan, you know the routine. You fill in a long form, scan your CNIC and your marksheets, pay a fee challan at a bank, upload a photo of the stamped receipt, and then wait. Somewhere on the other side, an admissions officer opens your file, checks every document, and decides.
In the summer of 2026 I spent a month as a Full Stack Developer intern at XCL Technologies in Hyderabad, working on exactly that kind of system: a university admission and examination portal. Registration, document uploads, challans, exam slips, and a Super Admin panel to run it all. I learned more in that month than in a semester, mostly because real users were going to touch it.
When the internship ended I wanted to know if I could build the whole thing on my own, end to end, and then take it further than we had time for. That became Ahmer Institute of Technology, or AIT. The institute is fictional. The code, the security and the tests are real, and the source is on GitHub.

What it does
AIT follows one admissions cycle from start to finish.
An applicant registers, fills in the application, ranks up to three programs and uploads their documents. An admissions reviewer opens the file, looks at each document and approves or rejects it with a note. Approval gives the new student an ID like 26BSSE007 and a one-time password, and enrols them in their first-semester subjects. The student signs in, is made to pick their own password, and then sees their classes, timetable, attendance, marks and fee challans.
Teachers take attendance and enter marks, but only for the subjects an admin assigned to them. A Head of Department sees their own department. The admin runs fees, exam slips, receipts, appeals, the timetable and the public website copy, and can hand parts of that work to sub-admins.
So it's really six things sharing one database: a public site with 14 pages, an applicant portal, a student portal, a teacher portal, a staff portal and an admin console. The database has 31 tables and 2 reporting views.

Why plain PHP
At XCL we used Laravel. For AIT I chose plain PHP and MySQL with no framework, on purpose. A framework hands you sessions, CSRF protection and safe queries for free, and I wanted to understand each of those by writing them myself. It runs on a normal XAMPP install, which is what most students in my class have on their laptops, and it also runs in Docker.
The cost is that nothing protects you by default. Every form, every query and every file download had to be made safe by hand, and that shaped most of the project.
Uploads were the hardest part
An admissions portal asks strangers to send you files. That's the riskiest thing a website can do. A file called photo.jpg can be a PHP script with a fake name. A PDF can carry JavaScript. An image can hide extra data after its last pixel.
Every upload in AIT goes through one pipeline, in this order:
- Only JPG, PNG, WebP and PDF are allowed.
- The first bytes of the file have to match its extension. A script renamed to
.jpgfails here. - Images have a 5 MB limit and PDFs 10 MB.
- The file is sent to ClamAV, an antivirus scanner. If the scanner is down, the upload is refused instead of being let through.
- Images are decoded and saved again as fresh WebP files, which throws away hidden data and metadata. PDFs are rebuilt with Ghostscript, and any PDF with scripts, attachments, forms or encryption is rejected.
- The file gets a random 128-bit name and is stored outside the website's public folder.
- It can only be downloaded through one page that checks who you are. Students get their own files. Admins get them only if their role allows it.

Two clicks at the same moment
Some bugs only happen when two things happen at once. Two admins open the same application and both press "Approve". An applicant double-clicks "Submit" on a slow connection. Without care you get two student IDs for one person, or two applications in a cycle that should only allow one.
AIT handles this in two places. The database refuses duplicates outright: one application per student per cycle, one challan per application, unique CNICs and emails. And every action that two people could race locks the row first (SELECT ... FOR UPDATE), so the second click waits, sees the work is done, and stops. Student IDs come from the database's own primary keys, so two approvals can never hand out the same number.
Who can do what
The admin console has sub-admins with nine separate permissions. One sub-admin might only handle admissions, another only teachers. Each page checks the permission on every request, and sub-admin access can expire on a date.
Teachers are limited to their own classes. That one sounds obvious, but my first version only checked that you were a teacher, not that the student was in your subject. The end-to-end tests caught it, which I'll come back to.
The timetable checks for clashes too. If a new slot overlaps another class with the same teacher or in the same room, the admin is told which subject it clashes with and has to pick another time.

Also readThe Discount That Knew You Were Hesitating: Rule-Based Personalisation in an Online Pharmacy
The Last Mango Problem: Race Conditions in a Real Marketplace
The rest of the security
The list is long, so here are the parts I'd want to know about as a user:
- Passwords are hashed with Argon2id. After three wrong attempts you get a captcha, after five the account locks for a while.
- Every form carries a secret token so another website can't submit it on your behalf, and the server also rejects posts that the browser marks as cross-site.
- A strict Content Security Policy means that even if someone sneaks HTML into a field, the browser won't run their script.
- Every form has a rate limit per account and per address, plus a hidden honeypot field and a timing check that bots usually fail.
- Files like
.env,.git, SQL dumps and the backend folder are never served to the web.
Making it fast and findable
The public site is what parents and students actually see first, so it gets the same care as the portals. Every page has its own title and description, canonical URL, social sharing card, breadcrumbs and structured data that tells search engines this is a university with programs and an FAQ. The sitemap is generated, not written by hand.
For speed there are several cache layers: OPcache for PHP itself, Redis for sessions, rate limits and data, APCu as a fast local layer, and a full-page cache for visitors who aren't signed in. Images are responsive WebP, and text is compressed with Brotli.
The whole stack runs with one command in Docker: Apache and PHP 8.5, MySQL 8, Redis 7 and ClamAV. The image is published to GitHub Packages.

Testing with fourteen people
This is the part I'm proudest of. I wrote 58 Playwright tests that play out a whole admissions cycle with fourteen made-up people: applicants who make every mistake, a parent using the contact form, admins with different permissions, a teacher, a Head of Department, a security guard, and an attacker named Eve who tries everything she can think of. There are also 23 backend tests, including one that uploads a real antivirus test file and checks ClamAV catches it.
Those tests found bugs I had no idea existed. Applicants were sent to a page that didn't exist after signing in. Account lockouts never held, because MySQL and PHP disagreed about the time zone. Teachers could mark students outside their class. About one temporary password in 500 broke my own password rules. I wrote about the people and the bugs they found in a separate, less technical post: I invented 14 people to break my university website.
GitHub runs both suites against the full Docker stack on every push.
What I got wrong
I trusted the happy path. For months I tested by clicking through as one well-behaved applicant. Every serious bug lived somewhere that person never went: a second admin, a teacher from another department, a wrong password, a slow double-click.
I mixed design systems. The public site, the student portal and the admin console were built at different times and still look like three slightly different products. A real university would want one look. I made them consistent in light and dark mode, but I never unified them.
"Coverage" is a slippery word. Every page, route and portal action is exercised by a test. Line-by-line code coverage isn't measured, so I can't claim every line has been run. If I did it again I'd set that up on day one.
By the numbers
- 6 connected parts: a public site and five portals
- 31 database tables and 2 reporting views
- 58 end-to-end tests with 14 personas, and 23 backend tests
- 15 bugs found and fixed in the v2.0.0 round, most of them by the tests
- 7 steps every uploaded file has to pass
Where it stands
AIT is finished. Version 2.0.0 is the final release and the repository is now archived, which means it's read-only and kept as it is. It did what I wanted it to do: it took a month of internship work and turned it into something I understand from the first line to the last.
The source, the screenshots and the Docker image are on GitHub.
Up next
Keep reading.
01I Invented 14 People to Break My University Website
A plain-language story about testing a university admissions website with fourteen imaginary people, from a careless applicant to a would-be hacker, and the real bugs they found that I never would have.
7 minThe Engineering Logs
02Zovita+: Building an Online Pharmacy That Adapts to Each Shopper
How I rebuilt Zovita+, an online pharmacy with 1,186 real medicines, a 3D symptom body map, personal offers, drug-interaction warnings, refill reminders, pharmacist-reviewed prescriptions, Urdu, and card or cash payments, on Laravel, React and MySQL: stack, data model, checkout locking, payments, admin, security, SEO, testing, what broke and what I'd change.
22 minThe Engineering Logs
03One Seat, Two Buyers: Race Conditions, IDOR and Keys in a Cinema Booking System
How BookMyMovie makes sure one cinema seat is never sold twice, a guessed booking number shows nothing, and a leaked or missing key can't quietly break tickets: row locks, lock order, a NULL-tolerant unique key, status compare-and-set, idempotency, IDOR scoping, HMAC tickets, captcha step-up, and the bugs that still got through.
11 minThe Engineering Logs
04BookMyMovie: Building a Cinema Ticketing Platform
How I built BookMyMovie on my own, a Laravel, React and MySQL cinema booking platform with live seat maps, trailers, QR e-tickets that work offline, online payments and an Urdu interface: stack, architecture, data model, design, SEO, security, what broke and what I would change.
17 minThe Engineering Logs
05MarketClock: A Trading Calendar That Shows Its Work
How I built MarketClock on my own: a Flask and Next.js app that tells traders and back offices whether 30 stock exchanges are open, when they close early and when a trade settles, and that links every fact it stores to the page it came from.
9 minThe Engineering Logs
06HolidayOps: Building a Working Days API for 250 Countries
How I built HolidayOps on my own: a Django and React platform that tells payroll, HR and logistics teams whether any date is a working day in any country, with company calendars, iCal feeds, signed webhooks and a public site that needs no JavaScript.
9 minThe Engineering Logs
