When I started building CrewSpace, our team’s task app, permissions seemed simple. Admins can delete projects. Regular members can’t. So you show the delete button to admins and hide it from everyone else.
Done, right?
No. And this is one of the most useful lessons I’ve picked up from building software, so here’s why.
The button is not the door
A button is just a picture that sends a request when you click it. Something like “please delete project 42.” The request goes to the server, and the server does the deleting.
Hiding the button stops people seeing the picture. It doesn’t stop the request.
Anyone can open their browser’s developer tools (it’s one keyboard shortcut in every modern browser) and watch the requests an app sends. They can copy one, change it and send it again. If the server doesn’t check who’s asking, it’ll happily delete project 42 for whoever sent the request, admin or not.
Hiding the button is like removing the “Staff Only” sign from a door but leaving it unlocked. Most people won’t find it. The one person looking for it will.
The real rule
The rule I build around now:
The interface decides what you see. The server decides what you can do.
Every single request that changes something has to be checked on the server:
- Who is this person?
- Are they allowed to do this specific thing?
- Are they allowed to do it to this specific item?
Hide the button as well, by all means. It’s good design not to show people things they can’t use. But hiding is for tidiness. Checking is for security.
The sneakier version
Question 3 is the one people forget, and for CrewSpace it matters most.
CrewSpace hosts multiple companies. Imagine a regular user at Company A who is allowed to view projects. Their app asks for project 42, which belongs to them. What happens if they change the number to 43, which belongs to Company B?
If the server only checks “is this person allowed to view projects?” the answer is yes, and it hands over Company B’s data. The server needs to check “is this person allowed to view this project?” Same permission, different question, completely different result.
This mistake has a name, IDOR (Insecure Direct Object Reference), and it has caused a long list of real data leaks at companies much bigger than ours. It’s rarely clever hacking. It’s usually someone changing a number in a URL and being surprised it worked.
How to catch it in your own app
Log in as your least powerful user. Open the developer tools. Find a request that only an admin should be able to make, or one that fetches someone else’s data, and try it yourself.
If it works, you’ve found a hole. Better you than someone else.
Why this stuck with me
Building with AI coding agents makes it very easy to produce something that looks finished. The screens are right, the buttons are in the right places, it all works when you click through it normally. But looking finished and being safe aren’t the same thing, and the agent won’t always point out the gap unless you ask.
That’s the part of the job that stays with the human. Knowing which questions to ask, and asking them before someone else does.