Add user roles and a real admin panel to a Bolt.new app
How to store roles where users can't change them, enforce them with RLS and server functions, and build a Bolt.new admin panel that is more than a hidden page.
Leer en español →
A real admin panel in a Bolt.new app needs three things: roles stored where users can't edit them, database policies that check those roles on every request, and server functions for actions that need elevated access. Hiding an /admin page or an "Admin" button only changes what people see. Anyone with your app's public key can talk to the database directly, so the database is where the rule has to live.
This guide explains why hidden pages don't protect anything, where to store a role, the SQL for a simple and safe setup, how to handle admin-only actions, and how to test that a normal user really can't get in.
Why isn't a hidden admin page enough?
Bolt's database security documentation puts it plainly: anyone who visits your site can send requests to your database directly, without using your site's screens. A route guard in React that checks if (user.role === 'admin') stops honest users from wandering in. It doesn't stop someone who opens the browser's developer tools and calls the same table the admin page reads.
So treat the interface as convenience and the database as the lock. Every table the admin panel reads or changes needs a policy that checks the role on the server side.
Where should I store the user's role?
| Where | Can the user change it? | Verdict |
|---|---|---|
User metadata set at sign-up (user_metadata) | Yes, the user can update their own metadata | Never use for permissions |
A role column in profiles that users can update | Yes, unless that column is specially protected | Risky; a common AI-generated mistake |
A separate user_roles table with no write policy | No | Simple and safe |
| A claim added to the login token by an auth hook | No | Safe, more setup; see Supabase's RBAC guide |
The second row deserves attention. Many generated apps add role to the profile table and an update policy that lets users edit their own profile. That policy also lets them set role = 'admin' from the console. Check for it before anything else.
How do I set up roles safely?
Bolt Database runs on Postgres with the same policy model as Supabase, so the SQL below works in both. Ask Bolt to apply it, or run it yourself in the SQL editor (Query in Bolt's Tables view, or the Supabase SQL editor if you connected or claimed a Supabase project).
create table public.user_roles (
user_id uuid primary key references auth.users(id) on delete cascade,
role text not null default 'member' check (role in ('member', 'admin'))
);
alter table public.user_roles enable row level security;
-- Users can see their own role. No insert or update policy on purpose.
create policy "read own role" on public.user_roles
for select to authenticated
using ((select auth.uid()) = user_id);
create or replace function public.is_admin()
returns boolean
language sql stable security definer
set search_path = ''
as $
select exists (
select 1 from public.user_roles
where user_id = (select auth.uid()) and role = 'admin'
);
$;
Then use the function in the policies of each table the admin panel needs. For example, users read their own orders and admins read all of them:
create policy "owners or admins read orders" on public.orders
for select to authenticated
using ((select auth.uid()) = user_id or (select public.is_admin()));
Two details matter. security definer lets the function read user_roles regardless of the caller's policies. And set search_path = '' avoids the "Function Search Path Mutable" warning that Bolt's database Security tab reports, the same warning Supabase's Security Advisor shows.
Making the first admin
Because no policy allows writes to user_roles, the app itself can't promote anyone. That's the point. Create the first admin by hand: find your user ID in the database's User Management screen (you can search by email), then run:
insert into public.user_roles (user_id, role)
values ('<your-user-id>', 'admin');
How do I handle admin-only actions?
Some actions need more than reading rows: deleting a user, changing someone's role, issuing a refund. They need the secret key, which bypasses RLS, so they belong in a server function. The pattern is always the same:
- The admin panel calls the server function with the signed-in user's session.
- The function identifies the caller from that session and checks
is_admin()(or readsuser_roles). - Only if the check passes does it use the secret key, stored in Secrets, to do the work.
- It writes a row to an
admin_audit_logtable: who did what, to which record, and when.
Never pass "I am an admin" as a parameter from the browser. The function has to work it out from the session.
What should I ask Bolt for?
Bolt does well with this when the prompt is specific. For example:
Add an admin area at /admin. Roles live in the user_roles table, checked
with the is_admin() function. Do not add a role column to profiles and do
not create insert or update policies on user_roles. The admin area lists
all orders and lets admins change an order's status. Enforce access in RLS
policies, not only in the React route.
Then lock the files that hold the role logic in Code view (Lock file), so a later prompt about colors doesn't rewrite them.
How do I test that it actually works?
- Create two test accounts: one admin, one normal user.
- Sign in as the normal user and open
/admin. You should be redirected or see nothing. - Still as the normal user, open the Network tab, copy a request the admin page would make and replay it. The response should be empty or an error, never the data.
- Try to update your own row in
user_rolesfrom the console. It should fail. - Run the database Security check in Bolt and resolve anything it flags, especially
RLS Policy Always True.
If step 3 returns data, the policy is missing or too broad. Our guides to Supabase RLS policy examples and one user seeing another user's data show how to track it down.
Quick answers
Can I just check the user's email to decide who is admin? It works for one person, but it breaks when emails change and spreads a hardcoded rule across the code. A roles table is barely more work.
Do I need roles beyond admin and member? Add them when you need them, for example staff with read-only access. If customers belong to companies, roles usually go per company; see multi-tenant apps on Supabase.
Is Bolt's User Management screen an admin panel? It's for you as the project owner, inside Bolt. Your app's staff need their own admin area in the app.
What else should an admin panel have? An audit log, two-step sign-in for admin accounts and a short list of who has access. More in our admin panel security guide.
Want a second pair of eyes on your permissions?
Roles are where small mistakes expose the most data. Our Supabase security review checks every table and policy, and Bolt app rescue covers the rest of the app. Start with the free health check or contact us.


