When "Signed In" Is Doing a Permission Check's Job

RedHub AI Editorialupdated October 4, 20266 min read

A hotel guest stands in the staff office in front of every drawer of the records cabinet pulled open, lit red.
Jump to a section8

When a route treats a valid session as permission to touch one specific record, signed in is not authorization: the login is doing a permission check's job, and it can't. The two only line up on routes where every signed-in user may see everything. That stops being true the moment a second customer's data lands in the same table.

TL;DR: A signed-in check answers "is this a real user." A permission check answers "may this user touch this record." Two routes on the same document can miss the identical check and still land on different verdicts, LOGGED-IN ONLY and NOBODY IS CHECKING, and the only difference is where the record's id comes from. Across a whole app, the worst route sets the verdict, not the average.

The same document, two routes, two verdicts

Take one saved document and two actions on it: editing it and deleting it. Both routes require a signed-in session. Neither compares the document's owner to the person asking. On paper, that's the same mistake made twice. In the worked example that ships with Logged-In Is Not Allowed, the two routes land in different places.

Edit route (R-04)Delete route (R-05)
Requires a signed-in sessionYesYes
Compares the document to the callerNoNo
Where the document id comes fromThe request, as a long random stringThe request, as a number that runs 1, 2, 3
VerdictLOGGED-IN ONLYNOBODY IS CHECKING

Both routes miss the same line of logic, and only one is graded as if it had no protection at all. That is the correct read, and the reason behind it decides what you fix first.

Why the identical gap gets two verdicts

The edit route is broken in a way that needs someone to already have, or stumble on, a specific document's id. The delete route is broken in a way that needs nothing. Sign up for a free account, count from one, and every document in the table is reachable in order. The login requirement isn't protecting the delete route from anything, and calling it LOGGED-IN ONLY like its sibling would understate how little stands in the way.

So the read-off treats a countable id with no ownership check the same as a route with no signed-in check at all. Both read NOBODY IS CHECKING, because both offer the same protection, which is none. The login screen looks identical either way.

Three verdicts for a whole app

Roll every route up and the app lands in one of three places. PERMISSIONS HOLD means no route reads LOGGED-IN ONLY or NOBODY IS CHECKING. SIGNED IN IS DOING TOO MUCH means at least one route stops at signed in on a record that could be someone else's, and none is worse. ANYONE CAN WALK IN means at least one route reads NOBODY IS CHECKING. The roll-up doesn't average. A stranger only needs one open door, so nine good routes don't change what happens at the tenth.

The kit's ten-route worked example plays out like this. Three routes read NOBODY IS CHECKING: viewing a customer order with no signed-in check, the countable delete route, and a third that the next section explains. Two read LOGGED-IN ONLY. The other five pass: downloading an invoice, opening a saved document and changing the account email read AUTHORIZED, while opening the dashboard and submitting feedback read MEMBERSHIP IS ENOUGH and never count against the app. The verdict is ANYONE CAN WALK IN, the worst of the ten, because that's the honest answer to "can anyone reach data that isn't theirs."

When you can't find the answer

Two routes in that sample show what "can't find" does. R-10 reads the team directory, a route that only touches data the whole account shares. Nobody could find whether it checks for a signed-in user, so it's scored as having no check and reads NOBODY IS CHECKING. R-08, an admin list of every user, does have a login check, but nobody could find anything confirming the caller's right to see that list. That unknown is scored as unchecked, and R-08 reads LOGGED-IN ONLY. An answer you can't find after a real look resolves to the unsafe side, because from a stranger's side of the request, "I couldn't find the check" and "there is no check" work the same way.

That rule has a cost, and the kit says so. It can make a result look worse than the app is: R-10 may well have a login check that nobody located. The dashboard counts the routes resolved from "can't find" separately, so you can see how much of the verdict is broken and how much is not yet looked at. The fix for the second kind is to go and look.

Audit routes as a group, not one at a time

The edit-and-delete split above tends to repeat across an app: several actions on one type of record, built at different times, guarded differently. Checking them as a group catches it.

  1. Group routes by record type. Documents, orders, invoices, whatever your app's core objects are. Test: list every action your app performs on each type: view, edit, delete, download, share.
  2. Check every action the same way. Test: for each action, find the login guard and the ownership comparison, as you would for a single route.
  3. Look for inconsistency in a group. Test: if viewing a document checks ownership but deleting it doesn't, you have one record type with a hole in it, and the hole is whichever action skipped the check.
Scope: this is a read-through of your own routes, grouped for consistency, not a live test of anything. It isn't a certification, and it doesn't run against your app on its own.

Hold the whole route list in one place

Grouping routes by hand works until you lose track past a handful. Logged-In Is Not Allowed holds the list for you: 80 rows, the same four questions on every one, a dashboard that rolls up to PERMISSIONS HOLD, SIGNED IN IS DOING TOO MUCH or ANYONE CAN WALK IN, and, where a route falls short, the one route to fix first instead of a wall of red.

Get Logged-In Is Not Allowed — $59

Pairs well with

A login check that only runs in the browser isn't a signed-in check at all, and The Browser Line Read-Off ($59) rolls those up to LINE HOLDS, LINE LEAKS or NO LINE. The Supabase RLS Read-Off ($69) reads the database layer underneath, where one browser-reachable service-role key rewrites the whole matrix to PROJECT OPEN. And the Vibe-Coded App Hardening Kit ($79) covers the rest of a production-readiness pass, with a 10-antipattern catalog and a 6-layer checklist. Our guide on how to secure an AI-built app walks the same ground in plain steps.

More in this guide

Frequently asked questions

What does it mean for "signed in" to be doing a permission check's job?

It means a route treats "this person has a valid session" as if it answered "this person may touch this record." Those are two different questions. It happens whenever a login check exists but nothing compares the record being requested to the person requesting it.

Why do two routes with the identical missing check get different verdicts?

Because the practical risk isn't identical. A route missing an ownership check is under-protected either way. If its record id can also be counted through, a free account and simple arithmetic reach every record in the table. A route with the same gap and a random id still needs someone to obtain a specific identifier first.

Does one bad route really outweigh nine good ones?

For the question "can a stranger reach data that isn't theirs," yes. A stranger only needs to find one route that doesn't check, not all of them. Averaging a bad route against nine good ones answers a different and less useful question.

What should I do if I can't tell whether a route checks anything?

Treat it the way you'd treat a confirmed missing check, then go and look again. If a real look through the code doesn't turn up a login guard or an ownership comparison, "I can't find it" and "it isn't there" have the same effect on anyone who tries the route.

Is this pattern specific to documents, or does it show up elsewhere?

It shows up on any record type with more than one action: orders, invoices, uploads, comments, anything a user can view, edit or delete. One record type often has some actions checked and others not, because each action tends to get built separately.

How it decides
Diagram of Logged In Is Not Allowed: ten routes, an enumeration gate, and a sample reading ANYONE CAN WALK IN.

The gate this post refers to, drawn from the tool’s own logic. See the tool.