I Removed Authentication and Shipped It as a Feature
The commit is a breaking-change feature commit. It deletes the login system and drops the owner columns from every table, and the README now says anyone who can reach the port can spend my provider key and read the corpus. I think it's the most secure thing I did to that app all summer.
Half a Fence
The app is a diagnostic tool for heavy equipment, with one operator. Along the way it had grown a multi-tenant skeleton, the way apps do when an agent is building them and every tutorial has a users table: sessions, access codes, a user identifier column on cases and machines and ledger rows.
Nothing enforced it. Queries didn't filter by owner, and the role model had one role. The access-code flow had a redemption race that could leave an orphan user row. That was fine, because the orphan got no grant and no session and nothing could reach it. But "fine because unreachable" is a sentence you should never have to say about your own login system.
Half-restored multi-tenancy that nothing enforces reads as a security control and isn't one. A reader sees a sessions table and a user column and assumes the queries are scoped. They weren't. The machinery itself was the hole, because it advertised a boundary that didn't exist.
The Delete
So the auth came out, all of it. The migration that removed it drops three columns that were declared not-null. A database that still had one would reject every insert the new code makes. I ran the migration against a copy of a real database, and 35 cases, 6 machines and 49 ledger rows survived the drop intact.
The tests now pin that a migration can't quietly declare an owner column again. The next time an agent decides the app needs users, a test fails and says why.
One migration number is a deliberate gap where the billing table was. Renumbering to close it would leave the git log unclear about which migration a commit meant, so the gap stays, with a comment.
What Stayed and Why
The origin check on state-changing requests stayed, for a new reason. With no session to ride, it isn't cross-site request forgery defense anymore. It's a spend guard. Three routes reach a billable provider key, and a page I happen to be visiting could otherwise POST to my own localhost in the background. Same code, different threat, documented as the different threat.
The per-case token ceiling stayed, and it's now the only spend limit in the app. An interview that never ends is the one failure that can still run up a bill while nobody's watching. The README says that.
The README says all of it up front. There's no sign-in. The dev command binds to localhost, and the start command binds to all interfaces. If you can reach the port, you can spend the key. Publishing an app that spends money on inference without saying so would be the actual defect.
The Access Code, Before It Died
The access-code model this replaced had one judgment call I want to record, because it's the kind of call that gets made silently.
The codes were described as one-time. I read one-time as the allowance, not the number of sign-ins. A code that stopped working after a single session would strand its holder the moment that session expired, and with no email in the app there's no reset to send and no way to prove who you were. So a code granted an allowance once and let you sign in as many times as you needed against it.
That call is gone with the rest of the auth. I still think it was right, and I still think it should have been written down at the time rather than pieced back together for a commit message.
The Reflex
When you find weak auth, the reflex is to strengthen it: add the scoping, add the roles, finish the fence.
I asked instead whether the app needed a fence. It has one operator, and that's me. The threat model is a page in my browser making a request I didn't intend, and a runaway interview spending money while I sleep. A users table solves neither. Two small guards and a README that tells the truth solve both.
A security control that doesn't control anything is worse than its absence. Its absence is at least honest.
-- Justin Higgins. Software Engineer, Midwest. Deleted the login and wrote down what the port can do.
Companion pieces: One Password for the Whole Family - the auth pattern this app did not need. Verify the Artifact, Not the Process - loopback is not a boundary.
Reactions, disagreements, war stories: jchigg2000.dev@gmail.com