Ph.D. Monitoring System
Live
Product · Web

Ph.D. Monitoring System

Lead Developer · Thapar Institute · 2024

Digitises the Ph.D. lifecycle at Thapar Institute: a Laravel 11 API covering supervisor allocation, IRB constitution, synopsis and thesis submission, patents and publications, with JSON-defined dynamic forms and multi-role approval chains behind a React admin client. The chain is data rather than code, so a regulation change is a row edit instead of a deploy. Google OAuth for sign-in, Cloudflare Turnstile on login and password reset, Excel export, self-hosted via Docker Compose, in production at phdportal.thapar.edu. A NestJS rewrite was scaffolded in early 2025 and shelved the same day — sizing it against 53 models, 202 routes and a 25,000-line SPA made it clear the Laravel codebase was cheaper to extend than to replace.

SCALE

~1,200 scholars · 50% less paperwork · 35% lower operating cost

Built with
LaravelLaravel
PHPPHP
MySQLMySQL
ReactReact
TailwindTailwind
MUIMUI
FlutterFlutter
Dart
DockerDocker
Jenkins
Project Details
RESOURCES

STATUS
Live
ROLE

Lead Developer

ORGANISATION

Thapar Institute

YEAR

2024

SCALE

~1,200 scholars · 50% less paperwork · 35% lower operating cost

TYPE

Product · Web

TAGS
Enterprise
Portal
Education
Full Stack

Thapar's Ph.D. programme ran on paper and email: supervisor allocation forms, IRB submissions, synopsis approvals, thesis milestones. Each one a document that had to find its way to the right desk, then back. This replaced that with a portal where the workflow itself is the record. It runs in production at phdportal.thapar.edu.

Why this is harder than a form builder

The naive version of this is a table of submissions and an approve button. That fails on the first real case. A Ph.D. form is not submitted to one approver; it walks an ordered chain of them, and the chain is different for every form. It can go backwards when someone rejects it. It has to stay editable by the person currently holding it and frozen for everyone behind them, while still being readable by everyone who already signed. Some forms can only be filed once in a scholar's life and some can be filed ten times. Some cannot be started until an earlier form has completed. And the same human being is often two roles at once, so "which queue is this in" depends on the hat they are currently wearing rather than on who they are.

That constraint - the chain is data, not code - shaped everything else. A regulation change should be a row edit, not a deploy.

The form engine

The core is a form engine rather than a set of hardcoded screens. Each process is a definition carrying its own field set, its own approval chain and its own repeat rules, so adding a stage is configuration rather than a release. Thirteen form types are registered today, and the chain is an ordered array of stages per form. Supervisor allocation is student, phd_coordinator, hod. IRB constitution runs six stages. Synopsis submission runs nine, ending at the Director. Semester off and thesis extension carry a repeat count of ten because a scholar can legitimately file them again; supervisor allocation carries one.

dynamicform.json is the contract the two halves agree on. A form is a document with a name, a slug, a prev_forms array naming the forms that must complete before this one can open, a flow array of role names, a current_level and maximum_level, a history, and a fields map keyed by role - so each role in the chain gets its own set of inputs on the same record. Every field carries a title, hint, default and current value, a type (text, date, autocomplete, multiple autocomplete), an is_editable flag, dropdown options, and a grid_space that decides how much of the row it occupies. Multi-select fields add minimum_count, maximum_count and unique, which is how "pick exactly six supervisor preferences, no repeats" is expressed as data rather than as validation code.

That shape is what the rest of the system hangs off. A form row records its current stage against an eleven-value enum - the ten roles plus complete - and per-role lock columns decide who can still edit it once it has moved on.

How a form knows whose desk it is on

Every form table is built from one shared migration trait, so they all carry the same routing skeleton: status, stage, completion, a steps JSON column, current_step and maximum_step, plus nine approval booleans, ten lock booleans and ten comment columns, one per role. Locks default to true for everyone except the student, who starts holding the pen.

The steps array is copied onto the row when the form is created rather than read from the registry at request time. That is the deliberate tradeoff: editing a chain does not retroactively reroute forms already in flight, which is the behaviour you want when a regulation changes mid-semester, at the cost of old forms running rules nobody can see in the config any more.

Access is then one line: the requesting role must appear in this form's own steps, at an index no greater than maximum_step. Because maximum_step is a high-water mark rather than a cursor, a role that has already signed keeps read access to the record forever while a role further down the chain cannot see it early. On top of that sits an identity check per role - a coordinator must coordinate that department, a head must head it, a supervisor must actually supervise that scholar.

Submitting is symmetric. Approving advances the stage, unlocks the next role and clears their approval flag. Rejecting requires a comment - the endpoint returns 403 without one - moves the form back a step, unlocks the role it landed on, and leaves maximum_step where it was so nobody loses visibility of a form that reversed.

What it covers

  • Thirteen form types from supervisor allocation through IRB constitution, revised IRB, synopsis, thesis submission, thesis and IRB extension, supervisor change, status change, title revision, semester off and the list of examiners.
  • Multi-role routing. Scholar, supervisor, department head, coordinator, DORDC, ADORDC, DRA, doctoral committee, external member and director each see a different queue over the same record.
  • Role switching in-session, because one person is routinely both a supervisor and a committee member; a user carries a base role, a current role and a cached expansion set.
  • Twenty-eight permission flags on the role table covering read, edit, add and delete scoped to all, department, or supervised scholars.
  • A forms index table carrying one row per scholar per form type with the stage, the filed count against max_count, and a <role>_available boolean per role - so every inbox in the system is a single indexed query instead of a walk through the chain.
  • Conditional filters on every listing, so a supervisor with forty scholars can find the three awaiting a signature.
  • Doctoral committee constitution with nominee cognates, outside experts, expert chairmen and departmental experts modelled as separate tables rather than free text.
  • Examiner recommendations captured as their own form with per-examiner detail rows.
  • Publications and patents tracked alongside the milestones, split into SCI journal, conference, book and patent entry paths.
  • Course and semester records including per-student course allocation and semester off-cycles.
  • Document uploads stored per form type under storage/app/public/uploads, filenames stamped as form, roll number, timestamp and a random suffix so two scholars uploading the same-named PDF in the same second cannot collide.
  • Bulk operations for student import, presentation scheduling and password resets, driven from spreadsheets parsed in the browser and processed through a queued job.
  • Notifications in-app and by email, fanned out by the stage a form just entered - the dispatcher resolves "supervisor" to every supervisor of that scholar, "doctoral" to the whole committee, "hod" to the department head - with templated Blade mails and support for scheduled sends.
  • Google OAuth for sign-in, both a redirect flow and a token endpoint for the SPA, Cloudflare Turnstile on login and password reset, and Excel export for anything an administrator needs to take off-platform.
  • An administrative override surface to create a form instance, force its stage, toggle its availability, disable it or delete it, because a chain will eventually wedge and somebody has to move it by hand.
  • A request log viewer in the admin surface, because the interesting failures are always somebody swearing they submitted something.

Architecture

Laravel 11 on PHP 8.2 with Sanctum tokens on a ten-day expiry, a Create React App front end using MUI and Tailwind, MySQL for storage, Docker Compose for the whole thing. The domain model is 53 Eloquent models and 34 controllers, with 319 lines of api.php delegating into thirty-two route files that declare 202 endpoints between them, over 65 migrations across sixty tables. The engine lives on the server; the client pairs a shared field and grid renderer with per-form, per-role screens, so a scholar's synopsis page and a DORDC's view of the same record are different components reading the same payload.

It runs self-hosted on university infrastructure rather than a cloud provider, which is what kept the operational cost down and what made the deployment story more interesting than the application one. The image is php:8.2-apache with pdo_mysql, mod_rewrite and a bundled Apache vhost, Composer copied in from the official image, and the application directory bind-mounted so a fix does not need a rebuild. Pushes to main trigger a Jenkins build on the same box. Dr. Tarunpreet Bhatia mentors the project.

Scale and upkeep

Around 1200 scholars use it. The repository carries 414 commits between 24 March 2024 and 8 December 2025, which is twenty-one months of sustained maintenance and the longest continuous run of anything I have built. The migration timeline reads like a history of the requirements: students and departments in March 2024, the IRB tables in June, thesis extension and supervisor allocation in August, the forms index and notifications in October, examiners in January 2025, and a burst in November 2025 adding title revision, courses, areas of specialisation and outside experts. Uploaded documents on the server span October 2024 to November 2025 across fee receipts, IRB constitutions and submissions, synopses, theses and presentations.

The honest edges are visible in the same places. Upload folders are named from the form name the caller passes, which is how a directory called publicationPHD krenge ended up on disk next to fee_receipt. Two of the ten notification fan-out branches - DORDC and DRA - are declared and left empty, so those roles get the in-app record without the push. And the override endpoints exist precisely because the state machine cannot express every exception the regulations produce.

Most of the engineering effort went not into features but into making the approval chains survive the messy cases: a supervisor leaving mid-programme, a form that needs to go back two stages, a committee member who is also the head of department signing the same document twice in different capacities. A Flutter client shipped alongside the web app for scholars who wanted the notifications on their phone.

The rewrite that wasn't

In February 2025 this was going to be replaced by a NestJS service called Nexus. What is in that repository is nest new and nothing more: an AppController returning getHello(), an AppService returning Hello World!, a main.ts booting on process.env.PORT ?? 3000, and five runtime dependencies, all of them the ones the generator installs. Every file under the server directory carries the same two-minute timestamp, which is the length of a single nest new run. The generator's own git init is there with zero commits on it and no remote ever configured, and the client/ folder holds one file added a month later, a scratch XSS payload that does not even parse.

Sizing the rewrite is what killed it. Reimplementing 53 models, 202 routes and a 25,000-line React SPA in a different framework buys nothing a scholar or a supervisor can see, and it means re-deriving rules that only exist as working code - there is no test directory and no factories, so the only specification of correct behaviour is the running system and the people using it. The honest read after scaffolding the project was that the Laravel codebase was cheaper to extend than to replace, so the extension work went back into the original. That call held up: the Laravel system has since taken 414 commits from nine contributors, and it was still getting new features nine months after Nexus was abandoned. Deciding not to build something is a real decision, and this one was made early enough to cost about an hour.

Project Details
RESOURCES

STATUS
Live
ROLE

Lead Developer

ORGANISATION

Thapar Institute

YEAR

2024

SCALE

~1,200 scholars · 50% less paperwork · 35% lower operating cost

TYPE

Product · Web

TAGS
Enterprise
Portal
Education
Full Stack