App-wide roles
Check permissions that apply across your application. You can use the result in a backend request handler, or optionally enforce the permission on a table with RLS.
First run the RBAC setup, including the role definitions and assignments. Keep the JavaScript constants or psql variables from that setup. The initial checks need no application table. Later SQL examples switch to authenticated to test the optional RLS policies.
Scenario 1: app-wide roles
Section titled “Scenario 1: app-wide roles”Alice is an admin, Bob is an editor and Carol is a viewer, all app-wide. Carol can read documents but cannot edit them:
const asCarol = permissions.as({ id: carol })const { data: canRead, error: readError } = await asCarol.rbac.hasPermission({ permission: 'documents.read',})if (readError) throw readError// canRead: trueconst { data: canEdit, error: editError } = await asCarol.rbac.hasPermission({ permission: 'documents.edit',})if (editError) throw editError// canEdit: falsebegin;select set_config('permissions.subject_id', :'carol', true);select permissions.rbac_has_permission(permission => 'documents.read');-- trueselect permissions.rbac_has_permission(permission => 'documents.edit');-- falsecommit;Your backend can use these answers before carrying out an operation. See Check access in your application for the request-handler flow, or continue with tenant-scoped roles.
Optional: enforce access with RLS
Section titled “Optional: enforce access with RLS”To enforce the same permissions on table queries, create policies where SELECT needs documents.read, UPDATE needs documents.edit, and DELETE needs documents.delete.
create table public.documents ( id uuid primary key default gen_random_uuid(), title text not null);insert into public.documents (title)values ('welcome'), ('holidays'), ('expenses');
alter table public.documents enable row level security;grant select, update, delete on public.documents to authenticated;
create policy documents_select on public.documents for select to authenticated using ( ( select permissions.rbac_has_permission( permission => 'documents.read' ) ) );create policy documents_update on public.documents for update to authenticated using ( ( select permissions.rbac_has_permission( permission => 'documents.edit' ) ) );create policy documents_delete on public.documents for delete to authenticated using ( ( select permissions.rbac_has_permission( permission => 'documents.delete' ) ) );The check does not depend on the row, so the policy wraps it in (select ...). Postgres then runs the check once for each query, as an InitPlan, and not once for each row. EXPLAIN shows an InitPlan and no SubPlan.
Each block below starts with the same two statements. The first statement starts a transaction as a client. The second statement sets the subject. The comments show the result:
begin;set local role authenticated;select set_config('permissions.subject_id', :'carol', true);select count(*) from public.documents;-- 3: viewers read all documentsupdate public.documents set title = upper(title);-- UPDATE 0: Carol cannot editselect permissions.rbac_has_permission(permission => 'documents.edit');-- falsecommit;
begin;set local role authenticated;select set_config('permissions.subject_id', :'bob', true);update public.documents set title = upper(title);-- UPDATE 3: editors can updatedelete from public.documents where title = 'EXPENSES';-- DELETE 0: editors cannot deletecommit;
begin;set local role authenticated;select set_config('permissions.subject_id', :'alice', true);delete from public.documents where title = 'EXPENSES';-- DELETE 1: admins can deletecommit;
begin;set local role authenticated;select set_config('permissions.subject_id', :'dave', true);-- Dave has no roleselect count(*) from public.documents; -- 0select set_config('permissions.subject_id', '', true);-- no subjectselect count(*) from public.documents; -- 0commit;In production, the set_config statement is part of your request setup (see Identity). On Supabase, the JWT does this.
Next step
Section titled “Next step”Continue with Tenant-scoped roles.