I want to start with the comment, because the comment set the tone for everything that followed.
It arrived on a GitHub issue in a repo that wasn't even the right project, filed by a dev.to reader who had apparently been quietly poking around my live deployment. It opened with "Heyho kid, isn't this like summer break or something?" and proceeded to describe, in the cadence of a friendly riddle, several things that were wrong with my application. It mentioned logs. It mentioned auth. It mentioned something about an axios interceptor that it found "funny."
It had the energy of a Saw trap, but polite. Like if Jigsaw also genuinely wanted you to pass.
I had never had anyone inspect my work like that before. As a newcomer to actually shipping things, you build in a kind of comfortable bubble where the only person who ever really uses what you made is you. Nobody is looking for the gaps. Nobody is checking the locks. And then, without warning, someone was — quietly, while I was going about my day, entirely without my knowledge — and they left a note.
It was humbling in a way I wasn't prepared for.
What they actually found
The comment flagged three things: a 422 error crashing the frontend, some kind of auth issue, and something subtle in the token refresh logic.
The 422 crash was quick to find once I looked. A 422 is an HTTP status code meaning "I understood your request but something about the data you sent me is wrong."1 When FastAPI validates incoming data and something doesn't pass — a password that's too short, a severity rating outside the allowed range — it doesn't just say "validation failed." It sends back a detailed list of objects describing exactly which field failed and why, something like [{"loc": ["body", "password"], "msg": "too short", "type": "string_too_short"}]. My frontend was taking that list and passing it directly to React to render as text. React can render strings as text. It cannot render a JavaScript object as text — it just crashes, the way a printer crashes if you hand it a sandwich instead of paper. Fix: check whether detail is a string or a list before trying to display it, and if it's a list, pull out just the human-readable message. Five minutes.
The auth issue turned out to be two things layered on top of each other. The first was a cookie security flag. When your browser stores a cookie, one of the settings you can attach to it is secure=True, which tells the browser to only ever send that cookie over an encrypted HTTPS connection and never over plain HTTP.2 I had set mine to secure=False during local development, because my laptop runs everything over plain HTTP and I needed the cookie to work locally. I deployed to production — which runs over HTTPS — and never changed the flag. One line, wrong value, sitting there since the first push, quietly making the refresh token cookie transmittable over any connection type, including ones that aren't encrypted.
The second was bigger. My frontend's route protection only checked whether something existed in browser storage under the token key. It never verified that the something was real. To understand why that matters: localStorage is a small key-value store built into every browser — basically a sticky note your web app leaves on the user's computer.3 Anyone can write to it. Open DevTools on any website, type localStorage.setItem('token', 'banana') into the console, and you've written "banana" to that site's storage. My route protection was checking whether the sticky note existed, not whether what was written on it was a real, cryptographically signed token the backend had actually issued. A sticky note that says "banana" and one that says "valid JWT" look identical to a check that only asks "is there a sticky note." Open the console, type one line, set a fake value, and the entire protected dashboard would render. The backend correctly rejected the fake token the whole time, so no real data was ever exposed. But the frontend never asked. The fix was making the route protection actually call the backend before rendering anything protected, rather than trusting whatever happened to be written on the note.
The third thing turned out to be the most interesting one to find, and I introduced it myself while fixing the auth bypass.
Here's the setup: when a request fails because the access token has expired, my app automatically tries to get a new one by calling a /auth/refresh endpoint. This all happens silently in a piece of code called an axios interceptor — a layer that sits between every outgoing request and its response, intercepting failures before the rest of the app ever sees them. The interceptor catches a 401 (which means "not authorized"), makes a refresh call, gets a new token, and retries the original request. Clean, automatic, invisible to the user.
The problem: the refresh call itself goes through the exact same interceptor, since it's made using the same shared API client. So when I tested the fix with a completely fake token — one that would fail both the original request and the refresh attempt — here's what happened: the /logs/ request returned 401, the interceptor tried to refresh, the /auth/ refresh request also returned 401, and the interceptor saw another 401 and tried to refresh that too. But refreshPromise, the variable tracking the in-flight refresh, was already set because we were literally in the middle of creating it. So the interceptor did await refreshPromise... waiting on a promise that could only resolve once the thing currently running had already finished.
It was waiting for itself.4 The page spun forever with no error and no explanation, because nothing was ever going to resolve a promise that depended on its own completion. The fix was a single condition: if the failing request is the refresh endpoint itself, skip the retry logic entirely and just reject immediately. One line. But finding it required reading console logs carefully enough to notice that the interceptor was firing twice — once for /logs/ and once for /auth/refresh — and asking why the second one was trying to refresh at all.
The thing they didn't find
Here's where it got interesting. While I was already in the codebase tracing every route, I found something the comment hadn't mentioned at all.
A quick piece of background: when you define a database table using SQLAlchemy, the Python library I use to talk to PostgreSQL, you write a Python class describing what the table should look like. Then you run a command called create_all(), which tells SQLAlchemy to look at all those classes and create the corresponding tables in the database — but only if they don't already exist. It's a one-way door. It creates, it never alters. If a table already exists and you later add three new columns to the Python class, create_all() sees the existing table, decides it's already there, and moves on without touching it.5 Your new columns stay in the code forever and never make it to the actual database.
I had added three new columns to the users table during development — role, is_verified, and verification_token — and manually rebuilt my local database to pick them up. But the production database on Railway had been running since before those columns existed. create_all() looked at it, saw a users table, and did nothing. Every time a real stranger tried to register on the live site, the backend tried to write to columns that didn't exist, the database threw an error, and the whole registration silently failed — no useful error message, no obvious cause, just a broken form on a deployed app that had been working in my local environment the whole time.
Three bugs found from one comment. One more found just from looking closely. None of them were the hard parts.
What slips past is usually simpler than what doesn't
That last part is the thing I keep turning over. The hard parts of building HealthTrack — the JWT refresh token system, the database schema design, the rate limiting, the role-based access control — those got my full attention. I thought carefully about them. I read documentation. I tested them.
The bugs that actually broke production were none of those things. They were:
A False that should have been True. A database migration step I forgot to run. A frontend check that trusted a string without verifying what the string meant.
The pattern is almost embarrassing once you see it: you spend so much focused attention on the genuinely complex parts of what you're building that the simple finishing steps slip right past you. Changing the database schema required real skill and thought. Remembering to rebuild the production database afterward was just a step. A simple, obvious, last step that I didn't do, and it broke everything.
I think about deployment differently now. When you finally finish something after hours of building, deployment feels like the finish line. You push, you wait, you see it live, and that rush of relief makes it feel like you're done. But deployment isn't the finish line. It's the beginning of the part where real people can see it, which means it's the part that requires its own careful attention, its own checklist, its own moment of "did I actually think about whether this setting makes sense in a production environment and not just on my laptop?"
Nobody told me that. Or maybe someone did and I wasn't listening yet, because I hadn't felt it yet.
The part that's genuinely freaky
Somewhere out there, while I was making lattes or reading or trying to get eight hours of sleep, a stranger was opening my application in a browser I'd never know about, typing test inputs into my registration form, watching the network tab, checking what my cookies looked like, and quietly noting which things didn't behave the way they should have.
That's the internet. That's what live means. The moment something is deployed, it stops being yours alone — it becomes a thing that exists in the world, accessible to anyone with a browser and enough curiosity to look closely. Most people don't look closely. But some do. And they will find the False that should have been True.
I find this both deeply unsettling and genuinely useful to know. Unsettling because the comfortable bubble of "only I ever use this" is gone the moment you push to production, and it doesn't come back. Useful because the alternative, not knowing, is so much worse. A stranger who finds your bugs and tells you kindly is the best possible outcome. It's better than a stranger who finds them and says nothing. It's better than finding out much later, in a context where the stakes are higher.
The comment closed with a suggestion to go skating and enjoy the summer. I did, eventually. But first I rebuilt the production schema, tightened the cookie flag, fixed the frontend auth check, resolved the deadlock I introduced while fixing the other things, set up a real email verification flow for actual strangers, and wrote a full technical breakdown of every issue as a GitHub comment before closing the issue as resolved.
Deployment is part of building. I know that now in a way I didn't before, because I felt it, which is the only way any of this really sticks.
Footnotes
1 HTTP status codes are standardized response codes servers send back to indicate what happened with a request. 4XX codes indicate client errors — 401 is "unauthorized," 403 is "forbidden," 422 is "unprocessable entity." MDN: HTTP response status codes
2 The Secure cookie attribute restricts the cookie to HTTPS connections only, preventing it from being transmitted over unencrypted HTTP. MDN: Using HTTP cookies — Secure attribute
3 localStorage is a Web Storage API that lets websites store key-value pairs in the browser with no expiration date. Unlike cookies, localStorage is never automatically sent to the server — it's purely client-side. MDN: Window.localStorage
4 This is a form of deadlock — a situation where two or more operations are each waiting for the other to complete, so neither ever does. Wikipedia: Deadlock
5 This is the distinction between schema creation and schema migration. create_all() handles creation only. For altering existing tables in production, you'd normally use a migration tool like Alembic, which tracks schema changes over time and applies them incrementally. Alembic: database migrations for SQLAlchemy
Top comments (2)
This is a great reminder that once an application is public, it is no longer running only in the environment you designed for.
Unexpected traffic, automated scanners, bots, and curious users become part of your production reality. One thing I’ve learned from building production systems is that observability should not be an afterthought.
A few practices that help a lot:
Track request patterns, not just errors
Add structured logs with useful context (user/session/request IDs)
Monitor unusual endpoint access and rate spikes
Set alerts for abnormal behavior
Keep audit trails for sensitive actions
For AI-powered applications, this becomes even more important because you also need visibility into model usage, token consumption, prompt patterns, and unexpected workflows.
The interesting part is that these “random testers” often reveal weaknesses you wouldn’t discover internally. Production traffic is sometimes the best stress test — if you have the right visibility.
Great story and a good reminder to treat every public app as a living system. 🚀
Luis, you seem to be an experienced dev, can you help me?
There's something in this story which does not add up...
The author of the article, , introduces herself like this:
Outside of code I write a technical blog, make lattes at a local cafe, do freelance illustrationIf you look at her other articles, she seems to be original... no direct product placement, no LinkedIn marketing.
A lot of things don't add up with her.
For example, she:
And the github issue... who'd mention Lupe Fiasco's Kick, Push and reference its lyrics... in a ticket about 4XXs and 5XXs?!
Can this be some sort of... call for a Hero's Journey perhaps?!
Maybe implying that she might have a more interesting skillset than a typical normie senior dev?!
Maybe that public education is in ruins, private education is a joke, so Gotham needs proper Teacher's Assistants?
You know the type of TA who honors the old ways, but spins it in her own modern way, to build rapport with younglings?
A sort of young TA, who still has a little bit of Dijkstra's vigor?
A sort of... idk ... pushback against dev.to's anime-ization, cute-ification and oversimplification of everything?
What do you think, Luis?
In a world dominated by:
npm i left-pador.map(f).map(g)moments we all adore and cherish,Would you be interested in @zdzhatdo doing some CS101 TA-ing here?
I'd personally really enjoy a cargo cult take down from this author, to make younglings realize that Senior devs are following patterns religiously, and they rarely involve metrics or actual Computer Science.
For example I rarely see the word 'invariant' on dev.to, let alone proof sketches, pairwise testing, or someone explaining that coverage is a nuanced topic and we have statement, decision, path etc. coverages.
I do not want to push any narratives onto the author, as I believe in artistic freedom.
But... a zdzhatdo John Wick Ballerina... well that would be a 10 out of 10 stars for me.
What about you Luis? Would you like that or something else perhaps?
Would you also be interested in seeing our heroine pull off an Ice Club Fight Scene?
Perhaps an Allen Ripley vs. useState-Yutani's dark anti-pattern secrets deep in React?
Or maybe you'd prefer a more calmer, and dark humor filled Frances McDormand detective type work about covariance and contravariance and why assertion libraries are so messed up?
What do you think Luis?
Do you think - given that she hones her literary skills and shows off her illustrative feats - can she make it a personal brand, or should she just fit in into the faceless mass of React devs?
Can you imagine - in the far future - she having a Substack or something yielding a lil bit of cash on the side for her artful escapades...
or should her writing be restricted only to PR descriptions and closed source corpo Confluence pages in the next 40 years?
Of course, we cannot and do not want to pressure her into anything, since she's a free citizen.
But, we can voice our subjective opinions.
What is your opinion Luis?
What would you like to see in zdzhatdo's cinema?