The sibling repo I forgot I had pushed
Two days ago I wrote /blog/streamlit-dbms-dual-database about a July 2022 DBMS mini-project. Users lived in MongoDB Atlas, orders lived in MySQL, the connection strings sat in plaintext at the top of main.py. I thought that was the one repo from that week. It was not. kaushiksaravanan/Academic-Project-DBMS-Hotel-Delivery-System is the sibling. Same domain, food delivery. Same era, initial commit 2022-07-01, ten commits total. Same account. Same Streamlit stack. Whether this is the earlier draft or the parallel submission a partner and I split, I do not remember. The git graph has ten commits and no tags.
What I can pin down is what is in the tree. Two files, main.py at 230 lines and admin.py at 88 lines, plus a duplicate admin/admin.py that I never cleaned up. About 400 lines across the three files. requirements.txt present, four pinned deps. The stack is streamlit==1.10.0, mysql-connector-python==8.0.29, pymongo==3.11.0, and dnspython==2.0.0. The exact Streamlit and driver pins as the sibling repo. That is not a coincidence, that was one pip freeze shared across two folders on my laptop.
What is different from the streamlit-dbms repo
The interesting difference is the split. In the sibling repo, users lived in Mongo and everything else lived in MySQL. In this repo the split is inverted. Auth, customers, hotels, foods, orders, delivery people, delivery companies all live in MySQL. Mongo is used only to look up food image URLs by name from a Food collection in a Dbms database. That is the whole document store's job in this app. Every meaningful read and write is a SQL statement, and Mongo is a decorative side lookup.
The other difference is the MySQL host. The sibling used sql6.freemysqlhosting.net, a shared free host where the account credentials are the database credentials. This repo uses PlanetScale, on k8ikuh3kl5mx.ap-south-2.psdb.cloud, database kau. That is a proper managed MySQL. It was on their free hobby tier at the time. PlanetScale killed the free tier in April 2024, so the branch this token was scoped to is almost certainly gone. Almost certainly is not the same as certainly, and a leaked token stays a leaked token whether or not the database still answers.
The MongoDB is the same Atlas cluster as the sibling repo. Same cluster0.max1z.mongodb.net, same <user>:<password> pair I flagged in the streamlit-dbms post, same Dbms database. If you already rotated the streamlit-dbms Atlas credentials, this repo used the same cluster and you are covered. If you did not, the credentials in this repo work equally well for both, and rotating one rotates both.
The schema is the DBMS-textbook set: customer, hotel, food, serves, orders, delivery_person, delivery_company. Register with a unique username against customer. Log in. Pick a hotel via a radio widget. Pick a food. Pick a quantity. On checkout, a delivery person is chosen with random.randint. Payment mode is cash on delivery, because coding a payment mode was outside the rubric.
The admin panel
There is a second app in this repo, admin.py. It talks to the same PlanetScale MySQL and lets an admin add hotels, add foods, wire foods to hotels through serves, and add delivery companies and delivery people. The gate on the panel is one if statement on the login form: username equals admin, password equals a four-digit pin baked into the source. Literal string comparison, no hashing, no rate limit, no lockout. The credentials are checked in code, not in a table.
Every college project I have ever seen has this exact shape. Username admin, a four-digit pin from the usual list of usual four-digit pins, both literals in the source. There is a reason. The rubric wants an admin flow, the lab session is three hours, and a real auth story is a week of work. So you write the gate that lets the demo happen and you write in the report that "authentication would be strengthened in a production deployment." Everyone reading the report knows the phrase means the pin is in the file. Everyone signs off. Ship.
The educational cost is that the pattern is sticky. If your first four projects hardcode a four-digit admin pin, the fifth one will too, because the muscle memory has been trained on shipping over correctness. The right shape even in a lab is a users table with a role column and passlib.hash.bcrypt. Two extra lines, one extra migration, and the habit forms in the direction that survives contact with a real deployment. I did not write it that way in 2022. I hope the next student does.
The credentials
At the top of main.py:
client = MongoClient("mongodb+srv://<user>:<password>@cluster0.max1z.mongodb.net/...")
mysql.connector.connect(
host="k8ikuh3kl5mx.ap-south-2.psdb.cloud",
user="<user>",
password="<password>",
database="kau",
)At the top of admin.py, the same MySQL block, verbatim. Placeholders in the quote above, real values in the file. The MongoDB credentials are the same <user>:<password> pair as the streamlit-dbms repo, on the same Atlas cluster. The PlanetScale credentials are a real password-formatted branch token that was live in July 2022.
The clean rotation, if you are in the same situation:
- Atlas, Database Access, delete the
adminuser, add a fresh one with a strong password. Force-disconnect active sessions on Network Access. If you rotated for the sibling repo this week, you are already done for this repo, they point at the same cluster. - PlanetScale, Dashboard, the
kaudatabase, Passwords, Regenerate. The old password stops working immediately. If the database is on a branch that was culled with the free tier in 2024, it is likely dead already, but rotate anyway because "likely dead" is not the same as "confirmed dead" and the token is a public artefact. git grep -E 'psdb\.cloud|mongodb\+srv' $(git rev-list --all)to confirm the string does not survive elsewhere.git filter-repo --replace-textto strip it from history, force-push, then rotate a second time so anything that already scraped the leaked value is invalidated.
Every query is an f-string
The whole file is this pattern:
cursor.execute(f"SELECT c_id, pwd FROM customer WHERE c_id = '{user_id}'")Registration, login, hotel list, food list per hotel, order insert, delivery-person lookup, admin add-hotel, admin add-food, admin wire-serves. Every one of them is f"...{value}...". The login form on main.py takes user_id from a Streamlit text input and drops it straight into the SELECT. A user_id of x' OR '1'='1 returns every row and the app happily logs the caller in as whoever comes back first. There is no other layer between the widget and the driver.
The parameterized version is the one from the sibling post. Placeholder on the query, tuple on the call.
cursor.execute(
"SELECT c_id, pwd FROM customer WHERE c_id = %s",
(user_id,),
)The driver escapes the value against the target dialect. The habit of writing this form even when the input feels safe is the entire safety mechanism, because the input feels safe until it does not, and by then the form is already in the file.
The topology
flowchart LR
A[Streamlit main.py] --> B{Login or register}
B -->|SELECT/INSERT customer| M[(PlanetScale kau<br/>k8ikuh3kl5mx.ap-south-2.psdb.cloud)]
A --> C[Hotel picker]
C -->|SELECT hotel/food/serves| M
C --> D[Checkout]
D -->|INSERT orders + random.randint dp| M
D -->|find_one image URL| N[(MongoDB Atlas<br/>cluster0.max1z.mongodb.net<br/>Dbms.Food)]
E[Streamlit admin.py] -->|literal PIN gate| E2[Admin panel]
E2 -->|INSERT hotel/food/serves/dp/dc| MWhat the two repos say together
One 2022 mini-project week, two Streamlit food-delivery apps, seven tables that look like a DBMS textbook, two managed databases per app, and the same set of anti-patterns in both. Plaintext credentials at the top of main.py. Passwords stored as-is in a pwd column. Every SQL call built with an f-string. A random-integer delivery-person picker that the report called an "algorithm." A hardcoded four-digit admin pin on the admin panel in the second one.
That is not one lesson repeated. It is one habit. The habit is that when the rubric asks for a working demo in three hours, everything else falls out. Credentials fall out because envvars are one more thing to explain. Parameterized queries fall out because f-strings are shorter. Hashing falls out because bcrypt is one more import. The habit shows up in every intro tutorial that skips the boring parts, and it shows up in every lab that lets the boring parts stay skipped. The way you un-train it is not to write a longer lab report. It is to make the shortest working demo also the one with an env var, a %s, and a hash. Once the shortest path includes those, the habit forms in the direction that survives.
I am rotating the PlanetScale token today, and I have already rotated the Atlas credentials for the sibling repo, which covers this one too. Both repos stay public because the point is the pattern, not the illusion that I never had it.
See also
- /blog/streamlit-dbms-dual-database: the sibling from the same July 2022 week. Same Atlas cluster, different MySQL host.
- /blog/leetcode-heroku-mongo-scraper: same credential-leak story, one year earlier.
- /blog/cipherstack-vault-full-architecture: where credentials live now so this stops happening.