Skip to content

My Admin Dashboard Is Just a Hidden URL, Is That a Problem?

6 min read

Your admin page isn't linked anywhere, so the only way in is to know the URL. Is that secure? No. An unlisted URL is not a lock, it is a curtain. Anyone who guesses /admin, finds it in your JavaScript, or gets a link forwarded to them can walk straight in. Before launch, the page needs a real server-side check that the visitor is actually an admin.

Why this happens

This is called security through obscurity: relying on a secret location instead of an actual permission check. It feels safe because you can't see the door, so surely no one else can either. But the door is fully functional, it is just painted the same colour as the wall.

Vibe-coded apps land here constantly. You asked the agent to "build an admin dashboard to manage users," and it built exactly that: a working page at /admin. You never said "and make sure only admins can open it," so it didn't add that part. The demo worked because you were the only person clicking, and nothing complained. The missing piece is invisible until a stranger finds it.

The catch is that /admin is one of the first paths anyone tries. Automated scanners hit it within minutes of your site going live. And your admin path isn't even secret: it is sitting in your front-end bundle, in the routing table, readable by anyone who opens the browser dev tools.

How to check

  1. Open your app in a private browsing window where you are not logged in. Type your admin URL directly. If the dashboard loads, or even flashes on screen before redirecting, it is unprotected.
  2. Log in as an ordinary, non-admin user and visit the same URL. A normal user should get a clean "not authorised" response, not the admin page.
  3. Open dev tools, go to the Network tab, and load the admin page. Look at the actual API calls it makes. If those endpoints return real data to a signed-out or non-admin request, the page isn't your only problem, the data behind it is exposed too.

If any of those three let you through, you are affected.

The fix

  1. Check the role on the server, not in the browser. A if (user.isAdmin) in your React code only decides what to draw. It does not stop anyone fetching the data underneath. The real gate lives where the data is served.
  2. Protect the data endpoints, not just the page. Every admin API route must verify the caller's role before returning anything. On Supabase, that means a Row Level Security policy or an edge function that checks the role. On Next.js, check the session and role inside the route handler or server action, and return 403 if it fails.
  3. Store the role where the user can't edit it. The admin flag belongs in your database, set server-side, not in a cookie or local storage the user controls. See why locking down who can become an admin matters.
  4. Deny by default. The admin route should assume "not allowed" and only open for a verified admin, so a new page you add next month is protected automatically instead of being open until you remember to lock it.
  5. Re-run the three checks above. Signed-out and non-admin visits should both be turned away, at the page and at every endpoint it calls.

The trap to avoid

The common wrong fix is to make the URL more hidden: rename it to /admin-x9f2, or add a check that only hides the menu link. Both leave the door unlocked, you have just made it harder to find, and it will still be found. A secret address is not authorisation. The only thing that counts is a server that refuses the request when the caller isn't an admin.

Where this fits

An unprotected admin page is one of the highest-severity gaps a beta can ship with, because it hands a stranger control over every other user. It is exactly what the free Readiness Report probes for: it checks whether your admin routes and their data endpoints actually reject an unauthorised caller, using your real project. It pairs closely with making sure a user can't tamper with the IDs in their own requests. If you would rather have the gaps fixed and verified in order, that is what the Finishing Pass is for.