Problem
A student's day is spread across notice boards and messages, and finding an empty room means walking the building.
LiveEducation platform1-minute read
Schedules, room availability, academic requests and announcements in one bilingual campus platform.

01
A student's day is spread across notice boards and messages, and finding an empty room means walking the building.
02
One phone-first screen answers a student's five daily questions: what is on today, where it is, which room is free now, what needs attention and what they can take part in. Everything else in the system exists to keep those answers correct, in Arabic and English.
03
Product design and full-stack delivery: domain model, availability engine, security and both languages.
One screen answers what is next, where it is and which room is free right now.
Live public demo on fictional data. The case study states its limits: no third-party security audit.
Student Guide is a bilingual campus platform built around a single idea: a student opening their phone between classes has five questions, and a university portal makes them work for every one of them.
The product answers all five on one screen — what is on today, where it is, which room is free right now, what needs attention, and what they can take part in — then gets out of the way. Everything else in the system exists to keep those five answers correct.
Everything. The product decision about which five questions mattered, the information architecture, the visual identity and design system, the domain model, the availability engine, the security model, the bilingual copy in both languages, the test suite, the deployment tooling, and the production operation of the live demo.
The original 2022 concept this revisits was a list of features — schedules, empty rooms, forms, elections, campus information. Delivered as a feature list, that becomes another portal.
So the brief was rewritten as five questions a student actually has, in the order they have them:
Every screen earns its place by answering one of those. The dashboard answers the first four above the fold; the room finder answers the third in one tap. Features that answered none of them — messaging, social feeds, gamification — were left out on purpose.
Five destinations, chosen so the mobile tab bar never needs a sixth: Dashboard, Schedule, Rooms, Requests, and an overflow sheet for Campus, Announcements, Elections, Notifications, Bookings and Settings.
Staff and administrators get additional surfaces in the same shell rather than a separate back office — a faculty member reviewing a request is one tap from the schedule they were already looking at.
Dashboard with a live countdown, today/week/agenda schedule with clash detection, the room finder, bookings and QR check-in, academic requests, elections, notifications and preferences.
Teaching schedule, today’s rooms with live availability, and a review queue: take a request into review, approve, reject or ask for changes, plus internal notes the student never sees.
Users and roles, buildings and rooms, courses, announcements that fan out as notifications, request types, elections and result visibility, analytics with CSV export, and the activity log.
This is the part of the product most likely to be wrong in a way nobody notices until two groups arrive at the same door.
There is no is_free column anywhere. A room is busy in a window when either a recurring class session on that weekday overlaps it, or a non-cancelled booking overlaps it. Overlap is the standard half-open comparison — a.start < b.end AND b.start < a.end — so 10:00–11:00 and 11:00–12:00 both fit, which is what a back-to-back booking should do.
Booking runs inside a transaction that first takes a row lock on the room. That serialises every concurrent attempt on the same room, so the overlap check that follows cannot be raced by a second request arriving a millisecond later. A unique key on the slot is a second, coarser backstop.
Why the lock matters. Checking availability and then inserting is two statements. Without the lock, two students can both pass the check before either inserts, and both get the room. The lock makes the check-and-insert atomic per room without blocking bookings for any other room.
The overlap matrix is covered by tests: a booking overlapping the start, the end, one fully inside, one fully containing, an identical one, and both back-to-back cases — plus zero-length and inverted windows, which are refused outright.
Five request types — activity, academic support, document, facility issue, student service — each declaring its own fields in JSON. That declaration drives both the rendered form and the server-side validation, so a field the type never declared cannot be submitted, and a payload carrying status or is_admin has those keys discarded rather than stored.
The workflow is a real state machine: draft, submitted, in review, changes requested, approved, rejected, withdrawn. Each transition names who may perform it, and the server enforces that — a student cannot approve their own request even by posting directly to the reviewer route.
Rejecting a request, or asking for changes, requires a reason. A student who is told “rejected” with no explanation has to start the conversation over, so the form will not submit without one.
Reviewers can add internal notes. Those are stored with a visibility flag and filtered server-side, and a test signs in as the requesting student to assert the note is absent from their view.
A student election module invites an obvious question — can whoever runs the database see how I voted? The honest answer had to be no, and it had to be structural rather than a promise.
So there are two tables. One records that a person voted, and carries a unique key on (election, user) — that key, not application code, is what enforces one vote each. The other records what was voted for, and holds no voter reference at all. The two cannot be joined. A tally is possible; attribution is not.
Tests assert this at the schema level: the ballot table has no user column, the voter table has no candidate column, the unique key exists, and a direct duplicate insert is rejected by the database itself. The audit log records that a user voted, and deliberately not for whom.
This is a sound design for a demonstration. It is not a certified voting system, and the case study says so in the limitations below rather than leaving it implied.
The demo accounts are printed on the sign-in page, which means anyone can sign in as an administrator. That is the point — and it is also the problem.
Demo mode refuses, server-side, the operations that would let one visitor spoil the demo for everyone else: creating or deleting users, changing roles, deactivating accounts, deleting core records, writing settings, sending mail. Each refusal is logged to the audit trail with the operation name and surfaced in an admin panel that lists what is blocked and what has recently been attempted.
Everything genuinely product-shaped stays enabled — booking rooms, submitting and reviewing requests, voting, publishing announcements, editing rooms — because a guarded nightly job restores a checksum-verified pristine dataset, which makes all of it reversible.
That reset refuses to run unless every guard passes: an exclusive lock, demo mode on, the target database named correctly, the snapshot matching its recorded checksum, the snapshot containing no database-level statements, and a least-privileged credential file present. It then verifies the restore left a non-zero user and room count.
default-src 'self' with no unsafe-inline anywhere — achievable because the campus map positions elements with SVG attributes and the analytics bars use generated width classes rather than inline styles.The security suite tests these as behaviour, not as configuration: XSS payloads through stored fields, SQL payloads through every search and filter, path traversal against attachment downloads, open-redirect attempts against the language and theme switches, and assertions that no database password, application key, password hash or server path appears on any rendered page.
514 translation keys per locale, with a test that fails the build if the two key sets diverge, if any value is empty, if the interpolation placeholders differ between locales, or if an Arabic value is still a copy of the English.
Content is bilingual in the data model rather than in a translation layer: courses, rooms, buildings, announcements and candidates all carry paired columns, so Arabic is real content and not a rendering of English.
The layout uses CSS logical properties throughout — margin-inline-start, border-inline-start, inset-inline-* — which is why the right-to-left view is a true mirror with no direction-specific overrides. Dates render in both Gregorian and Hijri calendars, and numbers use Arabic-Indic digits, through the intl extension.
What RTL actually broke. Two things, both found by looking rather than assuming. The select dropdown arrow was drawn with two CSS gradients forming a triangle; mirrored, the halves formed a broken mark, so it became a single symmetric SVG chevron. And the Arabic status filter row is wider than the English one — long enough to push the page sideways at 360px — so it now scrolls inside itself.
Designed at 360px first, because that is the width a student actually checks their timetable at. Verification was done by rendering the app in fixed-width iframes at true mobile viewports and measuring the result, rather than trusting a resized desktop window.
That measurement found real defects: a grid overflow caused by the default min-width: auto on grid children, tab labels wrapping and clipping at 360px, truncation that did nothing because text-overflow does not apply to inline boxes, and several controls under the 44px touch target. All fixed and re-measured.
prefers-reduced-motion respected; prefers-color-scheme honoured with an explicit override.PHP 8.3 on MySQL 8 with zero runtime dependencies. There is no composer.json because there is nothing to install: the router, service layer, validator, translator, migrator and test runner are part of the application.
That was a deliberate trade. A framework would have saved perhaps a week; not having one means this project still installs and runs years from now with git clone, a database, and two commands — and every line of its behaviour is inspectable in one repository.
The layering rule is simple and enforced by convention: controllers may call services, services may call each other and the core, and nothing in the service layer knows what HTTP is. That is why the availability engine can be tested directly, and why the seeder reuses the same conflict-safe booking path a live request uses — the demo data is generated through the production code, so it cannot contain a conflict the application would reject.
556 assertions across unit, feature and security suites, dispatched through the real kernel in-process — routing, middleware, CSRF, authorisation, controllers and views all take part. These are functional tests, not mocks.
The availability overlap matrix, check-in guards, schedule and clash detection, election one-vote enforcement and ballot secrecy, validation rules.
Public routes, authentication and throttling, every role against every protected route, the full request workflow, voting, notifications, demo-mode restrictions, locale parity.
A CSRF sweep over every state-changing route, injection and escaping, access control and file handling, headers and secret leakage.
The suite refuses to run against a production environment or a database not named for this project, and it restores any state it changes so a run leaves nothing behind.
The demo runs on a shared VPS alongside several unrelated applications, which shaped the deployment more than anything else: nothing SG does may affect a neighbour.
open_basedir that cannot reach another application.php-fpm -t and nginx -t before reloading anything, and refuses an artifact containing PHP in the document root or any world-writable file.Two bugs the deployment itself found. A defence-in-depth nginx rule denying source directory names silently matched the application’s own /app/ URL namespace, 404-ing every authenticated route — caught by verifying production rather than assuming it. And assets served immutable for a month meant a deployed CSS change would not reach a returning visitor, so asset URLs are now fingerprinted per release.
Verified on the live domain after deployment: every route for all three roles, both languages with correct text direction, HTTP to HTTPS redirect, a valid certificate chain over HTTP/2, the full security header set with no duplicates, no console errors, no Content-Security-Policy violations, and every co-hosted application unchanged against its pre-deployment baseline.
These are engineering results. There are no adoption or usage numbers here because the product has no users — it is a demonstration, and inventing traction for it would defeat the point of publishing the code.