More Projects
59 in total
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.
~1,200 scholars · 50% less paperwork · 35% lower operating cost
Project Details
Lead Developer
Thapar Institute
2024
~1,200 scholars · 50% less paperwork · 35% lower operating cost
Product · Web
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>_availableboolean 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
Lead Developer
Thapar Institute
2024
~1,200 scholars · 50% less paperwork · 35% lower operating cost
Product · Web