Skip to content
← Back to the blog
appssecurity

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.

By Devonauts Team5 min read
Leer en español →
A hand holds an access card up to an electronic reader mounted on a concrete wall

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?

WhereCan the user change it?Verdict
User metadata set at sign-up (user_metadata)Yes, the user can update their own metadataNever use for permissions
A role column in profiles that users can updateYes, unless that column is specially protectedRisky; a common AI-generated mistake
A separate user_roles table with no write policyNoSimple and safe
A claim added to the login token by an auth hookNoSafe, 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:

  1. The admin panel calls the server function with the signed-in user's session.
  2. The function identifies the caller from that session and checks is_admin() (or reads user_roles).
  3. Only if the check passes does it use the secret key, stored in Secrets, to do the work.
  4. It writes a row to an admin_audit_log table: 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?

  1. Create two test accounts: one admin, one normal user.
  2. Sign in as the normal user and open /admin. You should be redirected or see nothing.
  3. 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.
  4. Try to update your own row in user_roles from the console. It should fail.
  5. 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.

Still stuck with your app?

Run the free health check on your app's URL, or get the $299 App Rescue Audit: a senior engineer reviews the code and sends you a written report in 2 business days.