WeWeb implements RBAC by combining roles from your auth backend (Supabase, Auth0, or Xano) with User Groups defined in WeWeb. User Groups use AND logic — a user must have ALL roles in the group to qualify. Page-level access restriction requires WeWeb Scale plan. Frontend access control is UX only — enforce real authorization with Supabase RLS policies on your database tables.
Role-Based Access Control in WeWeb
Role-based access control (RBAC) in WeWeb has two layers: frontend access control (restricting pages and showing/hiding UI elements based on roles) and backend authorization (RLS policies that prevent unauthorized data access regardless of the frontend). The frontend layer uses WeWeb's User Groups — groupings of roles that you create in WeWeb and associate with pages. This tutorial covers setting up the complete RBAC stack: creating roles in Supabase, generating the required tables, creating User Groups in WeWeb, restricting page access, writing conditional visibility formulas for UI elements, and creating the RLS policies that actually protect your data. The security warning you will see repeated throughout: frontend role checks are UX, not security. A user who knows the page URL can still navigate there; only RLS prevents them from reading or writing unauthorized data.
Prerequisites
- Supabase Auth plugin installed and configured in your WeWeb project (see weweb-user-authentication tutorial)
- At least one test user account created in your Supabase project
- Understanding of what row-level security (RLS) means conceptually
- WeWeb Scale plan if you need page-level access restrictions
Step-by-step guide
Generate the roles tables in Supabase via WeWeb
Generate the roles tables in Supabase via WeWeb
The Supabase Auth plugin in WeWeb requires two specific tables to power role-based access: a `roles` table and a `users_roles` join table. WeWeb generates these automatically. In the WeWeb editor: Plugins → Authentication → Supabase Auth → click the 'Generate' button. This creates: `public.roles` (columns: id UUID, name TEXT, label TEXT, created_at TIMESTAMPTZ) and `public.users_roles` (columns: id UUID, user_id UUID references auth.users, role_id UUID references roles). After generation, verify the tables exist: open Supabase Dashboard → Table Editor → confirm both tables are present. If you already had these tables from a previous project, the Generate button skips creation and links to existing ones. Now add your roles: in Supabase Dashboard → Table Editor → roles → + Insert row → add rows for each role your app needs (e.g., name: 'admin', label: 'Administrator'; name: 'member', label: 'Member'; name: 'editor', label: 'Editor').
Expected result: The roles and users_roles tables exist in Supabase. Your role definitions are added as rows in the roles table.
Assign roles to users in Supabase
Assign roles to users in Supabase
To assign a role to a user, insert a row in the users_roles table linking the user's ID to the role's ID. In Supabase Dashboard → Table Editor → users_roles → + Insert row → user_id: paste the user's UUID (find it in Authentication → Users), role_id: paste the role's UUID (from the roles table). For assigning roles programmatically (e.g., assigning 'member' role on signup): create a Supabase Edge Function or a database trigger. The trigger approach runs entirely in Supabase without any WeWeb configuration: SQL editor → create function → create trigger on auth.users INSERT. Alternatively, in the WeWeb signup workflow, after the Sign up action, add a Supabase Database Insert action → table: users_roles → data: `{user_id: the-new-user-id, role_id: member-role-uuid}`. You can also assign roles through the WeWeb editor's user management interface if it appears in the Supabase Auth plugin settings — some plugin versions show a user list with role assignment controls.
1-- Injection point: Supabase SQL editor (run OUTSIDE WeWeb)2-- Auto-assign 'member' role when a new user signs up3-- This trigger runs in Supabase, not in WeWeb45-- First, get the member role ID:6-- SELECT id FROM public.roles WHERE name = 'member';7-- Replace the UUID below with the actual member role ID89CREATE OR REPLACE FUNCTION public.assign_default_role()10RETURNS TRIGGER AS $$11DECLARE12 member_role_id UUID;13BEGIN14 SELECT id INTO member_role_id15 FROM public.roles16 WHERE name = 'member'17 LIMIT 1;1819 IF member_role_id IS NOT NULL THEN20 INSERT INTO public.users_roles (user_id, role_id)21 VALUES (NEW.id, member_role_id);22 END IF;2324 RETURN NEW;25END;26$$ LANGUAGE plpgsql SECURITY DEFINER;2728-- Attach trigger to auth.users table29CREATE TRIGGER on_auth_user_created30 AFTER INSERT ON auth.users31 FOR EACH ROW32 EXECUTE FUNCTION public.assign_default_role();Expected result: Test users have roles assigned in users_roles. Signing up a new user via your WeWeb form automatically gives them the 'member' role.
Create User Groups in WeWeb
Create User Groups in WeWeb
WeWeb's User Groups translate backend roles into page-level access conditions. In the WeWeb editor: Plugins → Authentication → Supabase Auth → scroll to the User Groups section → + Add User Group. Name the group (e.g., 'Admins', 'Members', 'Editors'). In the group configuration, select which roles a user must have. IMPORTANT: User Groups use AND logic — a user must have ALL roles in the group to qualify. If your 'Super Admin' group requires both 'admin' AND 'editor' roles, only users with both roles qualify. For OR logic (admin OR editor can access), create separate groups for each role and assign both groups to the page. Create a group for each role combination you need for page access. Common setup: Group 'Admins' → requires role 'admin'. Group 'Members' → requires role 'member'. Group 'All Authenticated' → leave roles empty (any logged-in user qualifies). After creating groups, they appear in Page settings → Access control as options for page restriction.
Expected result: User Groups are created in the Supabase Auth plugin settings, each mapping to one or more backend roles.
Restrict page access by User Group
Restrict page access by User Group
WARNING: Page-level access restriction in WeWeb requires the Scale plan. On lower plans, this setting is available in the UI but does not enforce access — all pages remain accessible. With a Scale plan: select the page you want to restrict in the Pages panel → Page settings panel (right sidebar) → Access control section. Change 'Everybody' to the User Group(s) that should have access. Users not in any of the selected groups are automatically redirected to the page you set as 'Default page for unauthenticated users' in the Supabase Auth plugin settings. For multi-role scenarios: you can select multiple User Groups for a page — users in ANY of the selected groups can access it (OR logic at the page level). The AND logic applies within a single group. Also set the login redirect: if a non-authenticated user tries to access a protected page, they should land on the login page, not a 404. Verify the 'Default redirect for unauthenticated users' in the plugin settings points to your login page.
Expected result: Attempting to navigate to a restricted page while logged out redirects to the login page. Logged-in users without the required role are also redirected.
Show and hide UI elements based on user role
Show and hide UI elements based on user role
Beyond page-level restrictions, you often need to conditionally show UI elements within a page based on role — an admin action button, a billing section visible only to plan owners, or a 'Delete' control only for moderators. Use Conditional Rendering for this. Select the element → Settings panel → Conditional Rendering → click the plug icon → write a role-check formula. With Supabase Auth: `wwContext.user?.roles?.some(r => r.name === 'admin')`. This shows the element only for users who have the 'admin' role in their roles array. For multiple roles: `wwContext.user?.roles?.some(r => ['admin', 'moderator'].includes(r.name))`. For the inverse (hide from admins): `!wwContext.user?.roles?.some(r => r.name === 'admin')`. The roles array on wwContext.user is populated from the users_roles join table — this is why running 'Generate' in the plugin settings is important. SECURITY REMINDER: Conditional rendering hides elements from view but does not prevent API calls. Never put sensitive data in the frontend that non-admin users should never see — enforce data access in RLS.
Expected result: Admin-only buttons and sections are invisible to non-admin users. The DOM does not contain these elements when conditional rendering is used.
Create RLS policies to enforce backend authorization
Create RLS policies to enforce backend authorization
OUTSIDE WEWEB — in Supabase Dashboard: this is the critical security layer. Even if WeWeb restricts pages and hides elements, RLS policies ensure that database operations are authorized regardless of the client. Go to Supabase Dashboard → Table Editor → select a table → click 'RLS Policies' → + New Policy. For a table that admins can read all rows but regular users can only read their own: create two policies. Policy 1 (admin full access): `CREATE POLICY 'Admins can read all' ON public.your_table FOR SELECT USING (EXISTS (SELECT 1 FROM public.users_roles ur JOIN public.roles r ON ur.role_id = r.id WHERE ur.user_id = auth.uid() AND r.name = 'admin'))`. Policy 2 (users read own data): `CREATE POLICY 'Users read own' ON public.your_table FOR SELECT USING (user_id = auth.uid())`. Supabase applies both policies with OR logic — a row is returned if the user matches ANY policy. Run these in Supabase Dashboard → SQL editor. Test your policies using the Supabase Dashboard's Policy tester by simulating requests as different user IDs.
1-- Injection point: Supabase SQL editor (run OUTSIDE WeWeb)2-- Example RLS policies for a 'posts' table with admin and author roles34ALTER TABLE public.posts ENABLE ROW LEVEL SECURITY;56-- All authenticated users can read published posts7CREATE POLICY "Read published posts"8 ON public.posts FOR SELECT9 USING (10 status = 'published'11 OR user_id = auth.uid()12 OR EXISTS (13 SELECT 1 FROM public.users_roles ur14 JOIN public.roles r ON ur.role_id = r.id15 WHERE ur.user_id = auth.uid()16 AND r.name IN ('admin', 'editor')17 )18 );1920-- Only the post author can insert their own posts21CREATE POLICY "Authors can insert own posts"22 ON public.posts FOR INSERT23 WITH CHECK (user_id = auth.uid());2425-- Authors can update their own posts; admins can update any26CREATE POLICY "Authors and admins can update"27 ON public.posts FOR UPDATE28 USING (29 user_id = auth.uid()30 OR EXISTS (31 SELECT 1 FROM public.users_roles ur32 JOIN public.roles r ON ur.role_id = r.id33 WHERE ur.user_id = auth.uid()34 AND r.name = 'admin'35 )36 );3738-- Only admins can delete any post39CREATE POLICY "Only admins can delete"40 ON public.posts FOR DELETE41 USING (42 EXISTS (43 SELECT 1 FROM public.users_roles ur44 JOIN public.roles r ON ur.role_id = r.id45 WHERE ur.user_id = auth.uid()46 AND r.name = 'admin'47 )48 );Expected result: Database tables have RLS policies that enforce role-based access at the data level. Non-admin users cannot read, write, or delete records they are not authorized for, even if they bypass the WeWeb frontend.
Test your RBAC implementation end to end
Test your RBAC implementation end to end
Systematic testing prevents security gaps. Test each scenario explicitly: (1) Not logged in: navigate directly to each protected page URL — confirm you are redirected to login. (2) Logged in as 'member' role: navigate to admin-only pages — confirm redirect. Open browser dev tools → Network tab → look for Supabase API calls → confirm they return 0 rows or 403 errors for data you should not have access to. (3) Logged in as 'admin' role: navigate to admin pages — confirm access works. Confirm admin-only UI elements are visible. (4) Try a direct API call bypassing the UI: open browser dev tools → Console → run `fetch('https://your-project.supabase.co/rest/v1/your_table', {headers: {'apikey': 'your-anon-key', 'Authorization': 'Bearer your-non-admin-token'}}).then(r => r.json()).then(console.log)`. The response should only contain rows the non-admin user is authorized to see. If it returns all rows, your RLS policy has a gap. Also test role changes: assign a new role to a test user, log out, log back in, and verify the new access is reflected in wwContext.user.roles.
Expected result: All access scenarios are tested. Non-admin users cannot access admin pages or data via direct API calls. Admin users have correct access. Role changes reflect immediately after re-login.
Complete working example
1-- Injection point: Supabase SQL editor (run OUTSIDE WeWeb)2-- Reusable RLS helper function + common RBAC policies34-- Helper function: check if current user has a specific role5CREATE OR REPLACE FUNCTION public.has_role(role_name TEXT)6RETURNS BOOLEAN AS $$7BEGIN8 RETURN EXISTS (9 SELECT 110 FROM public.users_roles ur11 JOIN public.roles r ON ur.role_id = r.id12 WHERE ur.user_id = auth.uid()13 AND r.name = role_name14 );15END;16$$ LANGUAGE plpgsql SECURITY DEFINER;1718-- RLS for the roles table itself (read-only from frontend)19ALTER TABLE public.roles ENABLE ROW LEVEL SECURITY;20CREATE POLICY "Anyone authenticated can read roles"21 ON public.roles FOR SELECT22 USING (auth.role() = 'authenticated');2324-- RLS for users_roles (users see their own, admins see all)25ALTER TABLE public.users_roles ENABLE ROW LEVEL SECURITY;26CREATE POLICY "Users see own roles"27 ON public.users_roles FOR SELECT28 USING (user_id = auth.uid() OR public.has_role('admin'));29CREATE POLICY "Only admins manage user roles"30 ON public.users_roles FOR INSERT WITH CHECK (public.has_role('admin'));31CREATE POLICY "Only admins delete user roles"32 ON public.users_roles FOR DELETE USING (public.has_role('admin'));3334-- Example: profiles table (each user manages their own)35CREATE TABLE IF NOT EXISTS public.profiles (36 id UUID REFERENCES auth.users PRIMARY KEY,37 full_name TEXT,38 avatar_url TEXT,39 updated_at TIMESTAMPTZ DEFAULT now()40);41ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;42CREATE POLICY "Users manage own profile"43 ON public.profiles44 USING (id = auth.uid())45 WITH CHECK (id = auth.uid());46CREATE POLICY "Admins see all profiles"47 ON public.profiles FOR SELECT48 USING (public.has_role('admin'));Common mistakes
Why it's a problem: Assuming WeWeb page access restrictions provide security without RLS policies in Supabase
How to avoid: WeWeb's page access restriction is a frontend UX measure — it prevents the WeWeb page from rendering, but direct API calls to Supabase bypass it completely. Always pair page restrictions with Supabase RLS policies. Test by making raw Supabase API calls from the browser console with a non-admin auth token and verify they are blocked.
Why it's a problem: Forgetting that User Groups use AND logic, accidentally restricting a page to users with multiple roles simultaneously
How to avoid: If your User Group 'Power Users' requires roles 'editor' AND 'billing', a user with only 'editor' cannot access the page even if they should. For OR access (either role), create two separate User Groups and assign both to the page. The page-level assignment uses OR — any group match grants access.
Why it's a problem: Not running the 'Generate' button in the Supabase Auth plugin settings, causing wwContext.user.roles to be empty or undefined
How to avoid: The Generate button creates the roles and users_roles tables that the Supabase Auth plugin reads to populate wwContext.user.roles. Without these tables, the roles array on the user object is always empty. If you already have the tables from a manual schema migration, WeWeb should link to them automatically — but always verify by checking wwContext.user in the browser console after login.
Why it's a problem: Trying to use role-based page access on a WeWeb plan below Scale, wondering why it has no effect
How to avoid: Role-based page access restriction (Page settings → Access control → specific User Groups) requires the WeWeb Scale hosting plan. On lower plans, the setting is visible in the UI but not enforced. As a workaround on lower plans: implement a page-level On load workflow that checks wwContext.user.roles and calls a Navigate action to redirect unauthorized users.
Best practices
- Always combine WeWeb's frontend role checks with Supabase RLS policies — treat them as complementary, never as alternatives
- Create a has_role() helper function in Supabase SQL to simplify RLS policy expressions and avoid duplicating the role-check JOIN query
- Use AND logic within User Groups intentionally — most apps need OR-style access, which requires multiple single-role groups assigned to a page
- Test your RBAC by making direct Supabase API calls from the browser console with non-admin tokens — this is the only reliable way to confirm RLS is enforced
- Assign a default role (e.g., 'member') to all new users automatically via a Supabase database trigger so no user has an empty roles array
- Use optional chaining in all role-check formulas (wwContext.user?.roles?.some(...)) to prevent crashes when the user object is null during auth initialization
- Document your role hierarchy in a README or Supabase table description — role systems become complex quickly and future you will need the reference
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I'm implementing RBAC in WeWeb with Supabase Auth. My app has three roles: admin, editor, member. I need: (1) admins to access all pages, (2) editors to access a /content-manager page, (3) members to access only /dashboard. Explain how to set up User Groups in WeWeb, configure page access, and write the Supabase RLS policies for my 'posts' table where admins can read all posts, editors can read and write their own posts, and members can only read published posts.
In WeWeb with Supabase Auth, I've run the Generate button to create roles tables. I have roles: admin and member. My WeWeb User Group 'Admins' maps to the admin role. My page /admin is set to require the Admins group. But when I log in as an admin user and navigate to /admin, I get redirected to login. I'm on WeWeb Scale plan. What should I check? Also, what does wwContext.user.roles look like when roles are correctly assigned in Supabase?
Frequently asked questions
Can I assign multiple roles to a single user in WeWeb's Supabase Auth setup?
Yes. The users_roles table is a many-to-many join — one user can have multiple rows, each linking to a different role. Insert multiple rows in users_roles for the same user_id with different role_ids. In WeWeb, wwContext.user.roles will be an array with all the user's roles. Your role-check formula handles multiple roles: wwContext.user?.roles?.some(r => ['admin', 'editor'].includes(r.name)).
What happens to a user's access when I remove a role from them mid-session?
WeWeb's Supabase Auth plugin reads the user's roles at login time and stores them in the session. If you remove a role from a user in Supabase while they are actively using the app, their current session still reflects the old roles until they log out and log back in. For immediate revocation, you would need to invalidate the user's Supabase session via the Admin API (outside WeWeb), forcing a re-authentication. For most apps, session expiry (1 hour by default for Supabase JWTs) is sufficient.
Is there a way to implement role-based page restrictions in WeWeb without the Scale plan?
The official WeWeb page access restriction UI requires Scale plan. A workaround for lower plans: add a Project workflow triggered 'On page load'. Use a Custom JavaScript action to check wwContext.user and their roles. If unauthorized, use a Navigate action to redirect to the login or unauthorized page. This is less elegant than the built-in restriction but provides functional access control for apps not yet on Scale plan. Pair it with backend RLS policies for actual security.
How do I handle roles with Auth0 instead of Supabase in WeWeb?
With Auth0: define roles in Auth0 Dashboard → User Management → Roles. Assign roles to users in Auth0. Configure an Auth0 Action (post-login) to add roles to the user's ID token: event.user.roles. In WeWeb's Auth0 plugin, roles are typically available at wwContext.user['https://your-app.com/roles'] (namespaced claim). Create User Groups in WeWeb mapping to these Auth0 role names. Page access restriction and conditional rendering formulas work the same way — just update the path to the roles array in wwContext.user.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation