More Projects
59 in totalThapar CarPool
Backend / API · 2023
Ride-sharing for students travelling between the Patiala campus and nearby cities, shipped as a Flutter client against api.thapargo.tech. The schema splits the intent in two — a plain city hop, and a ride recorded against a train or flight number so people arriving on the same service could find each other rather than merely the same city, though that read side never landed as an HTTP route — and the API that serves it is a hand-rolled PHP MVC service written for shared hosting with no Composer and no shell: its own autoloader, router, sanitising and logging middleware, and a PDO layer where every column declares a type and a failed insert returns the whole list of problems instead of throwing on the first. A Create React App web front end covering the same six screens settled the information architecture and the two booking widgets, then was archived without ever calling the API.
Project Details
Backend / API
2023
Product · Mobile
Students heading out of Patiala coordinate in WhatsApp groups. Somebody posts a train number and a departure time and hopes another person on the same service scrolls past it before they book a cab alone. Thapar CarPool turns that into a lookup, and the interesting half of the build is the API, which was written from scratch in PHP with no framework under it.
Writing the framework was not a stunt. The deploy target was shared PHP hosting: no Composer,
no shell, no ability to install anything, a public_html directory and an FTP client. Under
that constraint Laravel is not an option and the realistic alternative is a pile of scripts,
each one re-opening its own PDO connection and re-inventing its own input checks. The middle
path is to write the four pieces a framework actually gives you - autoloading, routing, input
handling and a query layer - once, in about six files, and then have every endpoint after that
be five lines.
Two kinds of ride
The schema splits the intent in two, which is the design decision the whole product rests on.
wrap_go is the simple case: a date, a time band and a destination city. wrap_ride adds a
mode and a mode number, meaning a train or flight number, so people arriving on the same
service can be matched to each other rather than merely to the same city. Both tables hang off
a user id and carry a status column, set to pending when the row is created.
That split is what makes matching tractable. Matching on "going to Delhi on Friday" produces a list too loose to act on; matching on train 12232 produces a list of people who are provably going to be in the same place at the same minute. Keeping them as separate tables rather than one nullable column means the looser case never has to pretend it has a service number.
One way in
.htaccess sends every request that is not an existing file or directory to index.php, and
that file is the whole lifecycle in eleven lines. It pulls in the schema bootstrap, the
router and the two middlewares; sanitisation runs before the router is allowed to look at
anything; the router dispatches; then the request and the buffered response are both written
to disk. Nothing can reach a controller without passing through the same three steps, which is
the only structural guarantee a framework this small can offer.
Building the framework as well as the app
- Autoloading via
spl_autoload_register, translating namespace separators into directory separators relative to__DIR__and failing loudly on a missing class. - A router holding separate GET and POST pattern tables, matching each registered pattern
against the request URI with
preg_matchand spreading the captured groups into the handler as arguments. - Sanitising middleware that runs before the router touches anything, walking
$_GET,$_POSTand$_COOKIErecursively and pushingstrip_tagsover every leaf value. - Logging middleware appending the method, URI, headers and body of each request as a JSON line, then capturing the buffered response body and logging that too, which gives a replayable trace of every call the mobile client made.
- A PDO wrapper where each insert and update declares a type per column from a fixed
vocabulary (
int,phone,email,string,unique,empty,date,gst), validated before the statement is built. Phone numbers go through a regex accepting ten digits, or eleven behind a leading zero;uniqueruns a lookup against the target table first. - Errors as data. Every validator returns a pair of a boolean and a message naming the column it failed on, and the handler collects them into an array. A bad insert returns a list of problems rather than throwing, so a controller can hand the whole list back to the client in one response instead of failing on the first field.
- A debug switch. Flipping one static on the handler appends the table name, the submitted data, the datatype map, the field list and the generated SQL to that same error array, which is the entire debugging story on a host with no logs you can read.
- Partial updates by omission. Update methods take every column as a nullable argument and
build the
SETclause from whatever was not null, so the client can patch one field. - Schema as code. There are no migrations. A bootstrap file runs
CREATE DATABASE IF NOT EXISTSandCREATE TABLE IF NOT EXISTSfor users,wrap_goandwrap_rideon startup, so the schema is whatever that file says it is. - Duplicate guards at the controller layer. Registration rejects an email already in use; creating a ride first looks for an identical row on the same user, date, time and place and refuses rather than silently doubling it.
Two smaller decisions are worth naming. Prepared statements are real prepared statements -
PDO::ATTR_EMULATE_PREPARES is switched off - and every value additionally goes through
htmlspecialchars on the way in, so the sanitising middleware, the escaping in the handler and
the parameter binding are three independent layers doing overlapping work. And the query
builder keeps a raw queryRunner escape hatch, so anything the builder cannot express is one
call away rather than a reason to abandon the abstraction.
What the routes actually cover
Seven routes are registered, two of them health-check stubs that echo a string. The rest are
POST /user/check/exists-by-email, POST /user/register, POST /user/update-hostel,
POST /wrapgo/add and POST /wrapride/add. Every one of them writes.
The read side never landed as HTTP. The models carry getAllWrapGos, getWrapGoById,
getAllUsers, update and delete, all of them working, and none of them are exposed by a
route. So the committed API can record that four people are on train 12232 and cannot yet tell
any of them about the other three. The users table also carries an auth_token column that
no committed route reads or writes.
The clients
A Flutter app talking to api.thapargo.tech. Google sign-in over Firebase Auth, an OTP entry
screen, and an auth token cached in shared_preferences alongside the user's name, photo and
hostel, refreshed through a dedicated update endpoint so a returning user skips the login
screen. Screens cover the two ride types, bookings, profile, hostel change, payments, rewards,
donations, FAQ and settings, with every API response surfaced through toast messages driven by
a status boolean the server returns on every route.
The client is also the evidence that the deployed API ran ahead of the committed one: it calls
/user/login and /user/update, neither of which is in the repository, and expects back a
user object carrying the auth_token the schema has a column for. What is on disk is the
framework and the write path, not the last version that was live.
Alongside it, a Create React App front end for thapargo.tech covered the same six screens the Flutter client shipped: the landing surface, the two booking flows, bookings, a ticket view and profile. Six react-router routes, no layout chrome shared between them, each screen a self-contained component with its own stylesheet and a maroon accent on a light ground. The constraint that shaped every one of them is that this is a phone product rendered in a desktop browser. There is no responsive breakpoint anywhere in the stylesheets and no attempt at one - every screen is a fixed-width column of cards centred on the page, because the target was a web view wrapped as an app and the layout only ever had to work at one size. That decision buys a lot: the ticket view can be laid out like a cinema m-ticket, the booking flows look identical on Android and iOS, and nothing has to reflow.
Two widgets on those screens took all the actual work, and both are decisions rather than layout.
The dropdown is hand-rolled rather than a native <select>, because a <select> renders as a
system sheet on iOS and an in-page list on Android and the two look nothing alike; the commented-out
<select> blocks are still sitting in Wrapgo.js next to their replacements. It keeps an isOpen
boolean, rotates an inline SVG chevron through a CSS class and animates the list open, and its one
oddity is that selecting an option sets isOpen to true rather than false - the menu closes only
because the same click bubbles up to the wrapper's toggle handler. It works by accident rather than
by design.
The date picker is the more interesting call. Rather than validate a date after the fact, it refuses
to render one: a filterDate predicate greys out everything except the 9th, 10th and 11th of the
month, the pilot's trip window hard-coded into the calendar, so an invalid booking cannot be
expressed. Constraining the input instead of checking the output is the right instinct. Hard-coding
the dates into the component rather than passing them as a prop is not, and it is why the screen
would need editing rather than configuring for the next trip window. The web version was archived
there, once the Flutter client became the shipping surface and it was clear two teams were building
the same six screens against the same PHP API.
Honest state
The database credentials sit in a committed config class rather than an environment file. The
sanitising middleware tries to rewrite php://input after cleaning it, which is not a writable
stream, so a JSON body reaches the controller untouched while form-encoded input is cleaned -
and every route here reads form-encoded input, which is why it never mattered. The WrapGo
controller reads a $status variable its own signature does not declare, because the router
passes it as a fifth argument to a four-argument method and PHP quietly drops it. And around
30 KB of captured request and response logs are committed alongside the code, which is a
useful record of how the client actually called the server and not something that should have
been in version control.
The web front end never got past being a front end. None of its screens call the API, the
bookings list and the ticket contents and the fares are literals in JSX, and the dropdown's
selection never leaves component state. The train-and-flight toggle on the matching screen holds
its state in useState but neither button ever calls the setter, so the mode is fixed at
whatever it initialises to. The bookings screen renders the illustrated empty state and the
populated list at the same time, because there is no condition between them yet. The ticket
hotlinks its poster and its QR square from third-party CDNs and carries a literal booking
reference, train number and pickup hostel, and the profile is a placeholder identity with a stock
photograph. Its stylesheet imports reach into ../../../node_modules/ by relative path instead
of using the package specifier, which happens to work under Create React App and would break the
moment the build tool changed. And there is no index route, so the application renders nothing at
/ and every entry point is a deep link.
Project Details
Backend / API
2023
Product · Mobile