Case study

Faculty Appointment Portal

Students were walking to faculty cabins and waiting, only to find the faculty busy or away. The Faculty Appointment Portal lets them book a meeting ahead of time from the faculty member's actual free slots, saving time on both sides of the door.

ReactViteTailwind CSSNode.jsExpress.jsPostgreSQLJWTJestSupertestGitHub Actions
Role
Solo, full stack
Year
2025
Users
Students, faculty, admins
Tests
72, in CI
  • Double-booking blocked at the database level
  • 72 automated tests on every push
  • 3 roles, 1 codebase

The problem

Meeting a faculty member meant going to their cabin and hoping. If they were in a class, in a meeting, or not on campus, the student waited or came back later, and the faculty member was interrupted whenever someone did find them in.

What I built

A full-stack portal for three roles from one codebase. Faculty publish their weekly timetable once, and students only ever see the free time in it. A student picks a slot, a session length and a reason for the meeting; the faculty member approves or declines it; and admins oversee accounts and appointments across the institution.

  • Searchable faculty directory by name, department, expertise or building, with profiles and a cabin-location map.
  • Booking restricted to published office hours, with 15 to 60 minute sessions and status tracking from pending to completed.
  • Drag-to-paint weekly availability editor in 15-minute steps, plus per-date absences for leave and conferences.
  • Admin approval queue for new faculty, platform-wide appointment log, live dashboard counts and CSV export.
  • Email OTP password reset and appointment confirmations over SMTP.
Student dashboard: upcoming, past and total appointment counts, and recent appointments with Dr. Priya Nair marked Declined and Completed.
Student view. One request was declined because another student had already booked the slot.
Faculty dashboard: pending requests, upcoming, past and total appointments, a pending requests list, and buttons to manage appointments and update the profile.
Faculty view: requests to approve, and the week ahead.
Admin dashboard: total students, pending faculty approvals, approved faculty, and pending, upcoming, past and total appointments.
Admin view: approvals and platform-wide counts.

Architecture

A React single-page app talks JSON to an Express REST API, which owns every rule: role-based access, booking validation and conflict checks. PostgreSQL holds users, faculty profiles, appointments, one-time passcodes and revoked tokens; Multer stores profile photos and cabin maps on disk; an SMTP provider delivers OTP and confirmation emails. In production Express serves the built frontend too, so the whole app runs as one process.

System context diagram: students, faculty and administrators use the portal over HTTPS; the portal sends email through an SMTP provider and reads and writes PostgreSQL.
System context: who uses the portal and what it talks to.
Container diagram: the React SPA calls the Express API over /api; the API reads and writes PostgreSQL, stores uploads on disk with Multer, and sends email over SMTP/TLS.
Container view: the deployable pieces and how they communicate.

Data model

A user has at most one faculty profile; students book appointments and faculty receive them. Availability and absences live as JSON on the faculty row, so a timetable is one read. otp_tokens and revoked_tokens deliberately have no foreign keys: passcodes are matched by email, and revoked JWTs are blacklisted by their jti until they would have expired anyway.

Entity relationship diagram of the users, faculty, appointments, otp_tokens and revoked_tokens tables.
Database schema. Open the image for full size.

Keeping bookings consistent

Two students can try to book the same slot at the same moment, so booking and confirming both run as PostgreSQL transactions that take SELECT ... FOR UPDATE row locks before checking for conflicts. Confirming one request automatically declines every overlapping pending request in the same transaction, so a race always resolves to exactly one confirmed appointment.

An integration test reproduced a real Postgres deadlock under concurrent confirm requests. The fix was to lock rows in a consistent order on every code path. Moving the mark-completed job off a public, frequently polled endpoint and into a background job also removed a full-table update from the request path.

Security

Sessions live in an httpOnly, SameSite cookie, and logout revokes the JWT server-side by its ID. Role checks are enforced in API middleware, not just the UI.

  • bcrypt password hashing and parameterized SQL throughout.
  • Rate limiting on login, OTP and registration, plus Helmet security headers and a CORS allow-list.
  • Identical responses for unknown emails and wrong passwords, so accounts cannot be enumerated.
  • Unapproved faculty are hidden from search and return 404 on direct access.

Testing and CI

72 Jest and Supertest tests: unit tests for time-slot math and pagination, and integration tests that hit a real PostgreSQL database through the full stack, including the locking behaviour. GitHub Actions runs lint and tests against a Postgres service container on every push, so a broken build cannot merge.