Lovable: your admin page is hidden, not protected
If the only thing stopping a stranger reaching /admin is that you did not link to it, you do not have access control.
Ask Lovable for an admin dashboard and you get one. Ask it to make sure only admins can open it, and you get a check in the UI — a conditional that hides the link, or redirects if a role does not match. That is a front-door sign, not a lock.
Why UI checks are not access control
Every check written in your React components runs in the visitor's browser, on code they control. They can navigate straight to the route, edit the JavaScript, or simply call the same Supabase query the page would have called. The data lives behind the database, so the database must be the thing enforcing who reads it.
Check it yourself, two minutes
Visit your own app as a stranger
- 1Open a private/incognito window so you are signed out.
- 2Type your admin URL directly: yourapp.com/admin, then /dashboard, /settings, /users.
- 3Note what happens — a login screen is correct, a rendered page is not.
- 4Now sign in as an ordinary, non-admin user and repeat.
- 5Finally, take any URL containing an ID (/orders/1041) and change the number. If you see someone else's record, that is a separate and serious bug.
The last step matters more than the first. Most Lovable apps do gate the admin page somehow; far fewer check that user A cannot request user B's row by changing a number in the address bar.
Fix it
Make my access control real instead of cosmetic.
1. List every route that is meant to be restricted, and show me exactly
where each one is enforced. Any route whose only protection is a
conditional render or a client-side redirect is not protected — mark it.
2. Move enforcement into the database with Row Level Security, so the rule
holds no matter who calls the query:
- admin-only tables: a policy checking the caller's role, not a flag
sent from the browser
- per-user rows: USING ((select auth.uid()) = user_id)
3. Store roles in a table the user cannot write to. If a role lives in
user metadata the client can edit, an ordinary user can promote
themselves — fix that first.
4. For every route that loads a record by id, add an ownership check so
requesting another id returns nothing rather than someone else's data.
Show me a list of routes, what enforces each one now, and what an
anonymous visitor and a normal signed-in user get for each.Where the role itself lives
The fix above only holds if the thing it checks cannot be edited by the person being checked. Supabase user metadata is writable by the user it belongs to — a role stored there means an ordinary account can grant itself admin, and every policy built on it collapses at once. Roles belong in a table the user has no write policy on.
A roles table the user cannot promote themselves in
create table user_roles (
user_id uuid primary key references auth.users on delete cascade,
role text not null default 'user'
);
alter table user_roles enable row level security;
-- people may read their own role, and nobody may write it from the browser
create policy "read own role" on user_roles
for select using ((select auth.uid()) = user_id);
-- admin-only table, checked against that table rather than a client value
create policy "admins read audit log" on audit_log
for select using (
exists (
select 1 from user_roles
where user_id = (select auth.uid()) and role = 'admin'
)
);Common questions
- Is hiding a page from the menu enough to protect it?
- No. If the only thing stopping a stranger reaching /admin is that you did not link to it, you do not have access control. Routes are guessable, and the common ones — /admin, /dashboard, /settings, /users — are the first thing anyone tries.
- My app redirects non-admins away from /admin. Is that secure?
- Not on its own. Every check written in your React components runs in the visitor's browser, on code they control. They can edit the JavaScript, or skip the page entirely and call the same database query it would have called. The data lives behind the database, so the database has to enforce the rule.
- How do I test my own protected pages?
- Open a private window so you are signed out and type your admin URLs directly. A login screen is correct; a rendered page is not. Then sign in as an ordinary non-admin user and repeat — this is the case that usually fails.
- What is the check people always miss?
- Changing an ID in the address bar. Take any URL like /orders/1041 and try /orders/1042. If you can see somebody else's record, you have a broken ownership check — most Lovable apps do gate the admin page somehow, and far fewer check this.
This is one check out of 40+
Paste your site address and we run the whole list from the outside — leaked keys, open databases, unprotected pages — then hand you one prompt that fixes what we find. Free, about 30 seconds, no signup.
Check my site — free