This commit is contained in:
Saxon Fletcher committed 2026-05-26 12:35:21 +10:00
1 parent 59af01a7a9
commit 146c22ee72
2 files changed
+171 -1

No files matched your search

+1 -1
View File
@@ -189,7 +189,7 @@ export const templates: Template[] = [
"content": "insert into public.role_permissions (role, permission)\nvalues\n ('owner', 'organizations.read'),\n ('owner', 'organizations.update'),\n ('owner', 'organizations.delete'),\n ('owner', 'members.read'),\n ('owner', 'members.invite'),\n ('owner', 'members.update'),\n ('owner', 'members.remove'),\n ('owner', 'projects.read'),\n ('owner', 'projects.create'),\n ('owner', 'projects.update'),\n ('owner', 'projects.delete'),\n ('admin', 'organizations.read'),\n ('admin', 'organizations.update'),\n ('admin', 'members.read'),\n ('admin', 'members.invite'),\n ('admin', 'projects.read'),\n ('admin', 'projects.create'),\n ('admin', 'projects.update'),\n ('admin', 'projects.delete'),\n ('member', 'organizations.read'),\n ('member', 'members.read'),\n ('member', 'projects.read'),\n ('member', 'projects.create'),\n ('member', 'projects.update')\non conflict do nothing;\n"
}
],
"readme": "# Multi-tenant RBAC template\n\nOrganization-scoped authorization for SaaS applications. Membership in an organization assigns a role, roles grant permissions, and RLS policies call `authorize(organization_id, permission)`.\n\n## Includes\n\n- Organizations and organization memberships\n- Organization-scoped roles and permissions\n- `create_organization()` bootstrap helper\n- `authorize()` helper for tenant-aware RLS policies\n- Example `projects` table with scalable tenant policies\n\n## Dependencies\n\nRequires **database** and **auth**."
"readme": "# Multi-tenant RBAC template\n\nOrganization-scoped authorization for SaaS applications. Membership in an organization assigns a role, roles grant permissions, and RLS policies call `authorize(organization_id, permission)`.\n\nUse this template when users can belong to more than one organization and their permissions should be different in each organization.\n\n## Includes\n\n- Organizations and organization memberships\n- Organization-scoped roles and permissions\n- `create_organization()` bootstrap helper\n- `authorize()` helper for tenant-aware RLS policies\n- Example `projects` table with scalable tenant policies\n\n## How it works\n\nEvery tenant-owned row stores an `organization_id`. The current user's role for that organization is stored in `organization_members`, and each role receives permissions through `role_permissions`.\n\nRLS policies should check both tenant membership and the requested action by calling:\n\n```sql\npublic.authorize(organization_id, 'resource.action')\n```\n\nThat helper returns `true` only when the signed-in user is a member of the row's organization and their role grants the requested permission.\n\n## Getting started\n\nCreate an organization from your app after the user signs in:\n\n```sql\nselect public.create_organization('Acme Inc', 'acme');\n```\n\nThe creator is automatically inserted into `organization_members` as the `owner`.\n\nInvite another user by inserting a membership row:\n\n```sql\ninsert into public.organization_members (organization_id, user_id, role)\nvalues (\n '00000000-0000-0000-0000-000000000000',\n '11111111-1111-1111-1111-111111111111',\n 'member'\n);\n```\n\nThe insert is allowed only if the current user has `members.invite` for that organization. Assigning elevated roles such as `admin` or `owner` requires `members.update`.\n\n## Default roles\n\nThe seed file grants these permissions:\n\n| Role | Intended use |\n| ---- | ------------ |\n| `owner` | Full organization, membership, and project control. |\n| `admin` | Manage organization settings, invite members, and manage projects. |\n| `member` | Read organization and membership data, and create/update projects. |\n\nAdjust `supabase/seed.sql` before production if your app needs a stricter default. For example, remove `projects.create` or `projects.update` from `member` for read-only members.\n\n## Writing RLS policies\n\nFor tenant-owned tables, add an `organization_id` column and enable RLS:\n\n```sql\ncreate table public.documents (\n id uuid primary key default gen_random_uuid(),\n organization_id uuid references public.organizations on delete cascade not null,\n title text not null,\n body text,\n created_at timestamptz default now()\n);\n\ncreate index documents_organization_id_idx\non public.documents (organization_id);\n\nalter table public.documents enable row level security;\n```\n\nAdd permissions for the new resource:\n\n```sql\nalter type public.app_permission add value 'documents.read';\nalter type public.app_permission add value 'documents.create';\nalter type public.app_permission add value 'documents.update';\nalter type public.app_permission add value 'documents.delete';\n\ninsert into public.role_permissions (role, permission)\nvalues\n ('owner', 'documents.read'),\n ('owner', 'documents.create'),\n ('owner', 'documents.update'),\n ('owner', 'documents.delete'),\n ('admin', 'documents.read'),\n ('admin', 'documents.create'),\n ('admin', 'documents.update'),\n ('admin', 'documents.delete'),\n ('member', 'documents.read')\non conflict do nothing;\n```\n\nThen write one policy per operation:\n\n```sql\ncreate policy \"Authorized members can read documents\"\non public.documents\nfor select\nto authenticated\nusing ((select public.authorize(organization_id, 'documents.read')));\n\ncreate policy \"Authorized members can create documents\"\non public.documents\nfor insert\nto authenticated\nwith check ((select public.authorize(organization_id, 'documents.create')));\n\ncreate policy \"Authorized members can update documents\"\non public.documents\nfor update\nto authenticated\nusing ((select public.authorize(organization_id, 'documents.update')))\nwith check ((select public.authorize(organization_id, 'documents.update')));\n\ncreate policy \"Authorized members can delete documents\"\non public.documents\nfor delete\nto authenticated\nusing ((select public.authorize(organization_id, 'documents.delete')));\n```\n\nUse `with check` on inserts and updates so users cannot create or move rows into organizations where they do not have permission.\n\n## Common policy patterns\n\nRead access for any organization member:\n\n```sql\nusing ((select public.authorize(organization_id, 'documents.read')))\n```\n\nWrite access for admins and owners only:\n\n```sql\nwith check ((select public.authorize(organization_id, 'documents.create')))\n```\n\nOwner-only access can be modeled as a permission that only `owner` receives:\n\n```sql\nalter type public.app_permission aLine truncated
},
{
"id": "auth-email",
@@ -2,6 +2,8 @@
Organization-scoped authorization for SaaS applications. Membership in an organization assigns a role, roles grant permissions, and RLS policies call `authorize(organization_id, permission)`.
Use this template when users can belong to more than one organization and their permissions should be different in each organization.
## Includes
- Organizations and organization memberships
@@ -10,6 +12,174 @@ Organization-scoped authorization for SaaS applications. Membership in an organi
- `authorize()` helper for tenant-aware RLS policies
- Example `projects` table with scalable tenant policies
## How it works
Every tenant-owned row stores an `organization_id`. The current user's role for that organization is stored in `organization_members`, and each role receives permissions through `role_permissions`.
RLS policies should check both tenant membership and the requested action by calling:
```sql
public.authorize(organization_id, 'resource.action')
```
That helper returns `true` only when the signed-in user is a member of the row's organization and their role grants the requested permission.
## Getting started
Create an organization from your app after the user signs in:
```sql
select public.create_organization('Acme Inc', 'acme');
```
The creator is automatically inserted into `organization_members` as the `owner`.
Invite another user by inserting a membership row:
```sql
insert into public.organization_members (organization_id, user_id, role)
values (
'00000000-0000-0000-0000-000000000000',
'11111111-1111-1111-1111-111111111111',
'member'
);
```
The insert is allowed only if the current user has `members.invite` for that organization. Assigning elevated roles such as `admin` or `owner` requires `members.update`.
## Default roles
The seed file grants these permissions:
| Role | Intended use |
| ---- | ------------ |
| `owner` | Full organization, membership, and project control. |
| `admin` | Manage organization settings, invite members, and manage projects. |
| `member` | Read organization and membership data, and create/update projects. |
Adjust `supabase/seed.sql` before production if your app needs a stricter default. For example, remove `projects.create` or `projects.update` from `member` for read-only members.
## Writing RLS policies
For tenant-owned tables, add an `organization_id` column and enable RLS:
```sql
create table public.documents (
id uuid primary key default gen_random_uuid(),
organization_id uuid references public.organizations on delete cascade not null,
title text not null,
body text,
created_at timestamptz default now()
);
create index documents_organization_id_idx
on public.documents (organization_id);
alter table public.documents enable row level security;
```
Add permissions for the new resource:
```sql
alter type public.app_permission add value 'documents.read';
alter type public.app_permission add value 'documents.create';
alter type public.app_permission add value 'documents.update';
alter type public.app_permission add value 'documents.delete';
insert into public.role_permissions (role, permission)
values
('owner', 'documents.read'),
('owner', 'documents.create'),
('owner', 'documents.update'),
('owner', 'documents.delete'),
('admin', 'documents.read'),
('admin', 'documents.create'),
('admin', 'documents.update'),
('admin', 'documents.delete'),
('member', 'documents.read')
on conflict do nothing;
```
Then write one policy per operation:
```sql
create policy "Authorized members can read documents"
on public.documents
for select
to authenticated
using ((select public.authorize(organization_id, 'documents.read')));
create policy "Authorized members can create documents"
on public.documents
for insert
to authenticated
with check ((select public.authorize(organization_id, 'documents.create')));
create policy "Authorized members can update documents"
on public.documents
for update
to authenticated
using ((select public.authorize(organization_id, 'documents.update')))
with check ((select public.authorize(organization_id, 'documents.update')));
create policy "Authorized members can delete documents"
on public.documents
for delete
to authenticated
using ((select public.authorize(organization_id, 'documents.delete')));
```
Use `with check` on inserts and updates so users cannot create or move rows into organizations where they do not have permission.
## Common policy patterns
Read access for any organization member:
```sql
using ((select public.authorize(organization_id, 'documents.read')))
```
Write access for admins and owners only:
```sql
with check ((select public.authorize(organization_id, 'documents.create')))
```
Owner-only access can be modeled as a permission that only `owner` receives:
```sql
alter type public.app_permission add value 'billing.manage';
insert into public.role_permissions (role, permission)
values ('owner', 'billing.manage')
on conflict do nothing;
```
Then use it in a policy:
```sql
using ((select public.authorize(organization_id, 'billing.manage')))
```
## Choosing permissions
Prefer permissions that describe product actions rather than database internals:
```text
projects.read
projects.create
projects.update
projects.delete
members.invite
billing.manage
```
This keeps policies stable when the schema changes and makes it easier to map UI actions to backend access.
## Notes
This template does not rely on JWT custom claims for authorization. The database checks the current user's organization membership at query time, so a user can safely have different roles in different organizations.
## Dependencies
Requires **database** and **auth**.