DEV Community

Cover image for Hiding the Button Isn't Authorization: Why You Must Gate the Request
Nwosa Emeka Afamefuna
Nwosa Emeka Afamefuna

Posted on • Originally published at emeka-nwosa.hashnode.dev

Hiding the Button Isn't Authorization: Why You Must Gate the Request

The most common frontend authorization mistake isn't showing a button to the wrong user.

It's fetching the data anyway.

Most teams think authorization means hiding UI elements from people who don't have permission. If a user can't see the "Delete User" button, the job is done.

It isn't. Open the network tab and you'll see the real story.

Three Layers, Not One

Frontend authorization has three separate gates:

  1. Render Gate — should the user see this button, page, or menu?
  2. Data Gate — should the app even attempt to fetch this data?
  3. Backend Gate — can the server verify the permission no matter what the client does?

Most teams nail the first and third gates, then completely skip the second.

The Problem

Picture an admin-only analytics dashboard:

const { data } = useQuery({
  queryKey: ["analytics"],
  queryFn: fetchAnalytics,
});

if (!hasPermission("analytics:view")) {
  return null;
}
Enter fullscreen mode Exit fullscreen mode

Notice what actually happens here: useQuery fires on mount, before the permission check ever runs. The return null hides the UI, but it does nothing to stop the request that already left the browser.

Every unauthorized visitor now:

  • Triggers an API call that was never going to succeed
  • Gets back a guaranteed 403 Forbidden
  • Learns the endpoint exists, which hands a would-be attacker a working map of your API surface to probe further
  • Wastes bandwidth and server cycles on a request with no possible good outcome
  • Fills your logs with noise that looks identical to a real authorization failure, making genuine incidents harder to spot

The UI says one thing. The network says another.

Closing the Gap: The Data Gate

The fix is one property:

const query = useQuery({
  queryKey: ["analytics"],
  queryFn: fetchAnalytics,
  enabled: hasPermission,
});
Enter fullscreen mode Exit fullscreen mode

Now the request itself never leaves the browser unless hasPermission is true. This is the layer teams forget, because it's invisible in the UI. Everything looks fine on screen right up until you check what's actually happening on the wire.

One catch: this only works if hasPermission starts out correctly. If your app optimistically assumes access is true before permissions have actually loaded, the query can fire before enabled ever flips to false. Which brings us to the next problem.

Don't Flash Forbidden UI

There's a subtler version of the same mistake. If your app assumes access by default, renders the button, then removes it once real permissions arrive a few hundred milliseconds later, users briefly see actions they were never supposed to have.

Fail closed instead:

if (permissionsLoading) {
  return <Skeleton />;
}

if (!hasPermission) {
  return null;
}
Enter fullscreen mode Exit fullscreen mode

Show nothing until permissions are actually known. It's a small detail, but it's the same root cause as the data gate problem: never assume access before you've confirmed it.

Why This Matters

Gating the request isn't just about performance. It improves:

  • Security posture, by not handing out information about endpoints that exist
  • User experience, by avoiding needless error states
  • Backend efficiency, by eliminating requests that were always going to fail
  • Consistency, so the frontend stops telling a different story than the backend

The Rule

Don't just hide the button.
Don't just secure the API.
Gate the request too.

Authorization isn't one check. It's three.

If you take one thing from this: go search your codebase for every useQuery sitting behind a permission check, and confirm it has an enabled flag. That's usually where the gap is hiding.

Top comments (0)