<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://tech.uvoo.io/index.php?action=history&amp;feed=atom&amp;title=RLS_example_3</id>
	<title>RLS example 3 - Revision history</title>
	<link rel="self" type="application/atom+xml" href="https://tech.uvoo.io/index.php?action=history&amp;feed=atom&amp;title=RLS_example_3"/>
	<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=RLS_example_3&amp;action=history"/>
	<updated>2026-04-24T04:39:42Z</updated>
	<subtitle>Revision history for this page on the wiki</subtitle>
	<generator>MediaWiki 1.35.2</generator>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=RLS_example_3&amp;diff=5649&amp;oldid=prev</id>
		<title>Busk: Created page with &quot;RLS pattern for PostgreSQL that uses the JWT `sub` as the external identity, maps it to an internal UUID `user_id`, and supports per-group CRUD permissions on `todos`. It’s...&quot;</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=RLS_example_3&amp;diff=5649&amp;oldid=prev"/>
		<updated>2025-09-18T15:20:13Z</updated>

		<summary type="html">&lt;p&gt;Created page with &amp;quot;RLS pattern for PostgreSQL that uses the JWT `sub` as the external identity, maps it to an internal UUID `user_id`, and supports per-group CRUD permissions on `todos`. It’s...&amp;quot;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;New page&lt;/b&gt;&lt;/p&gt;&lt;div&gt;RLS pattern for PostgreSQL that uses the JWT `sub` as the external identity, maps it to an internal UUID `user_id`, and supports per-group CRUD permissions on `todos`. It’s PostgREST-friendly (uses `request.jwt.claims`) and keeps trust/authorization entirely in the DB.&lt;br /&gt;
&lt;br /&gt;
# 1) Extensions, schema, roles&lt;br /&gt;
&lt;br /&gt;
```sql&lt;br /&gt;
-- One-time setup&lt;br /&gt;
create extension if not exists pgcrypto;  -- gen_random_uuid()&lt;br /&gt;
&lt;br /&gt;
-- App schemas&lt;br /&gt;
create schema if not exists app;&lt;br /&gt;
create schema if not exists todo;&lt;br /&gt;
&lt;br /&gt;
-- Typical PostgREST role model:&lt;br /&gt;
--   - `api_owner` owns objects&lt;br /&gt;
--   - `web_anon` is the run-time role used by PostgREST for authenticated users&lt;br /&gt;
do $$&lt;br /&gt;
begin&lt;br /&gt;
  if not exists (select 1 from pg_roles where rolname = 'api_owner') then&lt;br /&gt;
    create role api_owner login;&lt;br /&gt;
  end if;&lt;br /&gt;
  if not exists (select 1 from pg_roles where rolname = 'web_anon') then&lt;br /&gt;
    create role web_anon nologin;&lt;br /&gt;
  end if;&lt;br /&gt;
end$$;&lt;br /&gt;
&lt;br /&gt;
grant usage on schema app, todo to web_anon;&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
# 2) Identity mapping (internal UUID ⇄ external `sub`)&lt;br /&gt;
&lt;br /&gt;
```sql&lt;br /&gt;
-- Internal users table:&lt;br /&gt;
create table if not exists app.users (&lt;br /&gt;
  id                uuid primary key default gen_random_uuid(),&lt;br /&gt;
  external_subject  text not null unique,  -- the JWT `sub` (opaque string, not always a UUID)&lt;br /&gt;
  display_name      text,&lt;br /&gt;
  email             text,&lt;br /&gt;
  is_active         boolean not null default true,&lt;br /&gt;
  created_at        timestamptz not null default now()&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
-- Resolve the current user's internal UUID from the JWT claims.&lt;br /&gt;
-- Optionally auto-provision a user row if first seen.&lt;br /&gt;
create or replace function app.current_user_id()&lt;br /&gt;
returns uuid&lt;br /&gt;
stable&lt;br /&gt;
security definer&lt;br /&gt;
set search_path = public, pg_temp, app&lt;br /&gt;
language plpgsql&lt;br /&gt;
as $$&lt;br /&gt;
declare&lt;br /&gt;
  claims   jsonb;&lt;br /&gt;
  sub_txt  text;&lt;br /&gt;
  uid      uuid;&lt;br /&gt;
begin&lt;br /&gt;
  claims := nullif(current_setting('request.jwt.claims', true), '')::jsonb;&lt;br /&gt;
  if claims is null then&lt;br /&gt;
    return null;&lt;br /&gt;
  end if;&lt;br /&gt;
&lt;br /&gt;
  sub_txt := claims-&amp;gt;&amp;gt;'sub';&lt;br /&gt;
  if sub_txt is null then&lt;br /&gt;
    return null;&lt;br /&gt;
  end if;&lt;br /&gt;
&lt;br /&gt;
  -- Try to find existing mapping&lt;br /&gt;
  select u.id into uid&lt;br /&gt;
  from app.users u&lt;br /&gt;
  where u.external_subject = sub_txt&lt;br /&gt;
    and u.is_active;&lt;br /&gt;
&lt;br /&gt;
  if uid is not null then&lt;br /&gt;
    return uid;&lt;br /&gt;
  end if;&lt;br /&gt;
&lt;br /&gt;
  -- Optional &amp;quot;auto-provision on first use&amp;quot;&lt;br /&gt;
  insert into app.users (external_subject, display_name, email)&lt;br /&gt;
  values (&lt;br /&gt;
    sub_txt,&lt;br /&gt;
    coalesce(claims-&amp;gt;&amp;gt;'name', null),&lt;br /&gt;
    coalesce(claims-&amp;gt;&amp;gt;'email', null)&lt;br /&gt;
  )&lt;br /&gt;
  returning id into uid;&lt;br /&gt;
&lt;br /&gt;
  return uid;&lt;br /&gt;
end&lt;br /&gt;
$$;&lt;br /&gt;
&lt;br /&gt;
revoke all on function app.current_user_id() from public;&lt;br /&gt;
grant execute on function app.current_user_id() to web_anon;&lt;br /&gt;
grant select on app.users to web_anon;  -- (optional: allow users to read their own profile via RLS below)&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
# 3) Groups &amp;amp; membership&lt;br /&gt;
&lt;br /&gt;
```sql&lt;br /&gt;
-- Groups&lt;br /&gt;
create table if not exists app.groups (&lt;br /&gt;
  id          uuid primary key default gen_random_uuid(),&lt;br /&gt;
  name        text not null unique,&lt;br /&gt;
  created_by  uuid not null references app.users(id) on delete restrict,&lt;br /&gt;
  created_at  timestamptz not null default now()&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
-- Group membership&lt;br /&gt;
create table if not exists app.group_members (&lt;br /&gt;
  group_id  uuid not null references app.groups(id) on delete cascade,&lt;br /&gt;
  user_id   uuid not null references app.users(id) on delete cascade,&lt;br /&gt;
  role      text not null check (role in ('owner','manager','member')),&lt;br /&gt;
  added_at  timestamptz not null default now(),&lt;br /&gt;
  primary key (group_id, user_id)&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
create index if not exists idx_group_members_user on app.group_members(user_id);&lt;br /&gt;
create index if not exists idx_group_members_group on app.group_members(group_id);&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
# 4) Todos &amp;amp; per-group shares&lt;br /&gt;
&lt;br /&gt;
```sql&lt;br /&gt;
-- Core todos&lt;br /&gt;
create table if not exists todo.todos (&lt;br /&gt;
  id         uuid primary key default gen_random_uuid(),&lt;br /&gt;
  owner_id   uuid not null references app.users(id) on delete restrict,&lt;br /&gt;
  title      text not null,&lt;br /&gt;
  body       text,&lt;br /&gt;
  is_done    boolean not null default false,&lt;br /&gt;
  created_at timestamptz not null default now(),&lt;br /&gt;
  updated_at timestamptz not null default now()&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
create index if not exists idx_todos_owner on todo.todos(owner_id);&lt;br /&gt;
&lt;br /&gt;
-- Shares: grant group access to a todo with simple, explicit action flags&lt;br /&gt;
-- (r = read, w = update, d = delete). You can normalize further if preferred.&lt;br /&gt;
create table if not exists todo.todo_group_shares (&lt;br /&gt;
  todo_id   uuid not null references todo.todos(id) on delete cascade,&lt;br /&gt;
  group_id  uuid not null references app.groups(id) on delete cascade,&lt;br /&gt;
  can_read  boolean not null default true,&lt;br /&gt;
  can_write boolean not null default false,&lt;br /&gt;
  can_del   boolean not null default false,&lt;br /&gt;
  primary key (todo_id, group_id)&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
create index if not exists idx_todo_shares_todo on todo.todo_group_shares(todo_id);&lt;br /&gt;
create index if not exists idx_todo_shares_group on todo.todo_group_shares(group_id);&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
# 5) RLS policies&lt;br /&gt;
&lt;br /&gt;
Enable RLS:&lt;br /&gt;
&lt;br /&gt;
```sql&lt;br /&gt;
alter table app.users           enable row level security;&lt;br /&gt;
alter table app.groups          enable row level security;&lt;br /&gt;
alter table app.group_members   enable row level security;&lt;br /&gt;
alter table todo.todos          enable row level security;&lt;br /&gt;
alter table todo.todo_group_shares enable row level security;&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
## `app.users` (each user sees themselves; optional admin bypass if you have one)&lt;br /&gt;
&lt;br /&gt;
```sql&lt;br /&gt;
-- SELECT: only yourself&lt;br /&gt;
create policy users_select_self on app.users&lt;br /&gt;
for select&lt;br /&gt;
to web_anon&lt;br /&gt;
using (id = app.current_user_id());&lt;br /&gt;
&lt;br /&gt;
-- UPDATE: let users edit their own profile (optional)&lt;br /&gt;
create policy users_update_self on app.users&lt;br /&gt;
for update&lt;br /&gt;
to web_anon&lt;br /&gt;
using (id = app.current_user_id())&lt;br /&gt;
with check (id = app.current_user_id());&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
## `app.groups`&lt;br /&gt;
&lt;br /&gt;
```sql&lt;br /&gt;
-- CREATE a group: creator becomes owner via trigger or API-side insert&lt;br /&gt;
create policy groups_insert_any on app.groups&lt;br /&gt;
for insert&lt;br /&gt;
to web_anon&lt;br /&gt;
with check (created_by = app.current_user_id());&lt;br /&gt;
&lt;br /&gt;
-- SELECT groups if you're a member&lt;br /&gt;
create policy groups_select_member on app.groups&lt;br /&gt;
for select&lt;br /&gt;
to web_anon&lt;br /&gt;
using (&lt;br /&gt;
  exists (&lt;br /&gt;
    select 1&lt;br /&gt;
    from app.group_members gm&lt;br /&gt;
    where gm.group_id = app.groups.id&lt;br /&gt;
      and gm.user_id = app.current_user_id()&lt;br /&gt;
  )&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
-- UPDATE/DELETE groups only if you're an owner&lt;br /&gt;
create policy groups_update_owner on app.groups&lt;br /&gt;
for update&lt;br /&gt;
to web_anon&lt;br /&gt;
using (&lt;br /&gt;
  exists (&lt;br /&gt;
    select 1 from app.group_members gm&lt;br /&gt;
    where gm.group_id = app.groups.id&lt;br /&gt;
      and gm.user_id = app.current_user_id()&lt;br /&gt;
      and gm.role = 'owner'&lt;br /&gt;
  )&lt;br /&gt;
)&lt;br /&gt;
with check (&lt;br /&gt;
  exists (&lt;br /&gt;
    select 1 from app.group_members gm&lt;br /&gt;
    where gm.group_id = app.groups.id&lt;br /&gt;
      and gm.user_id = app.current_user_id()&lt;br /&gt;
      and gm.role = 'owner'&lt;br /&gt;
  )&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
create policy groups_delete_owner on app.groups&lt;br /&gt;
for delete&lt;br /&gt;
to web_anon&lt;br /&gt;
using (&lt;br /&gt;
  exists (&lt;br /&gt;
    select 1 from app.group_members gm&lt;br /&gt;
    where gm.group_id = app.groups.id&lt;br /&gt;
      and gm.user_id = app.current_user_id()&lt;br /&gt;
      and gm.role = 'owner'&lt;br /&gt;
  )&lt;br /&gt;
);&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
## `app.group_members`&lt;br /&gt;
&lt;br /&gt;
```sql&lt;br /&gt;
-- Anyone can see members of their groups&lt;br /&gt;
create policy gm_select_member on app.group_members&lt;br /&gt;
for select&lt;br /&gt;
to web_anon&lt;br /&gt;
using (&lt;br /&gt;
  exists (&lt;br /&gt;
    select 1 from app.group_members gm2&lt;br /&gt;
    where gm2.group_id = group_members.group_id&lt;br /&gt;
      and gm2.user_id = app.current_user_id()&lt;br /&gt;
  )&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
-- Add members only if you're owner/manager of that group&lt;br /&gt;
create policy gm_insert_mgr on app.group_members&lt;br /&gt;
for insert&lt;br /&gt;
to web_anon&lt;br /&gt;
with check (&lt;br /&gt;
  exists (&lt;br /&gt;
    select 1 from app.group_members gm2&lt;br /&gt;
    where gm2.group_id = group_members.group_id&lt;br /&gt;
      and gm2.user_id = app.current_user_id()&lt;br /&gt;
      and gm2.role in ('owner','manager')&lt;br /&gt;
  )&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
-- Update member roles (promote/demote) only if owner/manager&lt;br /&gt;
create policy gm_update_mgr on app.group_members&lt;br /&gt;
for update&lt;br /&gt;
to web_anon&lt;br /&gt;
using (&lt;br /&gt;
  exists (&lt;br /&gt;
    select 1 from app.group_members gm2&lt;br /&gt;
    where gm2.group_id = group_members.group_id&lt;br /&gt;
      and gm2.user_id = app.current_user_id()&lt;br /&gt;
      and gm2.role in ('owner','manager')&lt;br /&gt;
  )&lt;br /&gt;
)&lt;br /&gt;
with check (&lt;br /&gt;
  exists (&lt;br /&gt;
    select 1 from app.group_members gm2&lt;br /&gt;
    where gm2.group_id = group_members.group_id&lt;br /&gt;
      and gm2.user_id = app.current_user_id()&lt;br /&gt;
      and gm2.role in ('owner','manager')&lt;br /&gt;
  )&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
-- Remove members only if owner/manager (or yourself to leave)&lt;br /&gt;
create policy gm_delete_mgr_or_self on app.group_members&lt;br /&gt;
for delete&lt;br /&gt;
to web_anon&lt;br /&gt;
using (&lt;br /&gt;
  (user_id = app.current_user_id()) or&lt;br /&gt;
  exists (&lt;br /&gt;
    select 1 from app.group_members gm2&lt;br /&gt;
    where gm2.group_id = group_members.group_id&lt;br /&gt;
      and gm2.user_id = app.current_user_id()&lt;br /&gt;
      and gm2.role in ('owner','manager')&lt;br /&gt;
  )&lt;br /&gt;
);&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
## `todo.todos`&lt;br /&gt;
&lt;br /&gt;
```sql&lt;br /&gt;
-- READ if you own it OR you are in a group that has read share&lt;br /&gt;
create policy todos_select_owner_or_shared on todo.todos&lt;br /&gt;
for select&lt;br /&gt;
to web_anon&lt;br /&gt;
using (&lt;br /&gt;
  owner_id = app.current_user_id()&lt;br /&gt;
  or exists (&lt;br /&gt;
    select 1&lt;br /&gt;
    from todo.todo_group_shares s&lt;br /&gt;
    join app.group_members gm&lt;br /&gt;
      on gm.group_id = s.group_id&lt;br /&gt;
     and gm.user_id  = app.current_user_id()&lt;br /&gt;
    where s.todo_id = todos.id&lt;br /&gt;
      and s.can_read&lt;br /&gt;
  )&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
-- CREATE: only as yourself (owner_id must equal current user)&lt;br /&gt;
create policy todos_insert_self on todo.todos&lt;br /&gt;
for insert&lt;br /&gt;
to web_anon&lt;br /&gt;
with check (owner_id = app.current_user_id());&lt;br /&gt;
&lt;br /&gt;
-- UPDATE if owner OR group share allows write&lt;br /&gt;
create policy todos_update_owner_or_shared on todo.todos&lt;br /&gt;
for update&lt;br /&gt;
to web_anon&lt;br /&gt;
using (&lt;br /&gt;
  owner_id = app.current_user_id()&lt;br /&gt;
  or exists (&lt;br /&gt;
    select 1&lt;br /&gt;
    from todo.todo_group_shares s&lt;br /&gt;
    join app.group_members gm&lt;br /&gt;
      on gm.group_id = s.group_id&lt;br /&gt;
     and gm.user_id  = app.current_user_id()&lt;br /&gt;
    where s.todo_id = todos.id&lt;br /&gt;
      and s.can_write&lt;br /&gt;
  )&lt;br /&gt;
)&lt;br /&gt;
with check (&lt;br /&gt;
  owner_id = app.current_user_id()&lt;br /&gt;
  or exists (&lt;br /&gt;
    select 1&lt;br /&gt;
    from todo.todo_group_shares s&lt;br /&gt;
    join app.group_members gm&lt;br /&gt;
      on gm.group_id = s.group_id&lt;br /&gt;
     and gm.user_id  = app.current_user_id()&lt;br /&gt;
    where s.todo_id = todos.id&lt;br /&gt;
      and s.can_write&lt;br /&gt;
  )&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
-- DELETE if owner OR group share allows delete&lt;br /&gt;
create policy todos_delete_owner_or_shared on todo.todos&lt;br /&gt;
for delete&lt;br /&gt;
to web_anon&lt;br /&gt;
using (&lt;br /&gt;
  owner_id = app.current_user_id()&lt;br /&gt;
  or exists (&lt;br /&gt;
    select 1&lt;br /&gt;
    from todo.todo_group_shares s&lt;br /&gt;
    join app.group_members gm&lt;br /&gt;
      on gm.group_id = s.group_id&lt;br /&gt;
     and gm.user_id  = app.current_user_id()&lt;br /&gt;
    where s.todo_id = todos.id&lt;br /&gt;
      and s.can_del&lt;br /&gt;
  )&lt;br /&gt;
);&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
## `todo.todo_group_shares`&lt;br /&gt;
&lt;br /&gt;
```sql&lt;br /&gt;
-- Visible if you are the todo owner OR member of the shared group&lt;br /&gt;
create policy shares_select_visible on todo.todo_group_shares&lt;br /&gt;
for select&lt;br /&gt;
to web_anon&lt;br /&gt;
using (&lt;br /&gt;
  exists (select 1 from todo.todos t where t.id = todo_group_shares.todo_id and t.owner_id = app.current_user_id())&lt;br /&gt;
  or exists (select 1 from app.group_members gm where gm.group_id = todo_group_shares.group_id and gm.user_id = app.current_user_id())&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
-- Only the todo owner (or group owner/manager, if you prefer) can create/update/delete shares&lt;br /&gt;
create policy shares_cud_todo_owner on todo.todo_group_shares&lt;br /&gt;
for insert&lt;br /&gt;
to web_anon&lt;br /&gt;
with check (&lt;br /&gt;
  exists (select 1 from todo.todos t where t.id = todo_group_shares.todo_id and t.owner_id = app.current_user_id())&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
create policy shares_update_todo_owner on todo.todo_group_shares&lt;br /&gt;
for update&lt;br /&gt;
to web_anon&lt;br /&gt;
using (&lt;br /&gt;
  exists (select 1 from todo.todos t where t.id = todo_group_shares.todo_id and t.owner_id = app.current_user_id())&lt;br /&gt;
)&lt;br /&gt;
with check (&lt;br /&gt;
  exists (select 1 from todo.todos t where t.id = todo_group_shares.todo_id and t.owner_id = app.current_user_id())&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
create policy shares_delete_todo_owner on todo.todo_group_shares&lt;br /&gt;
for delete&lt;br /&gt;
to web_anon&lt;br /&gt;
using (&lt;br /&gt;
  exists (select 1 from todo.todos t where t.id = todo_group_shares.todo_id and t.owner_id = app.current_user_id())&lt;br /&gt;
);&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
# 6) Grants&lt;br /&gt;
&lt;br /&gt;
```sql&lt;br /&gt;
-- Base privileges (RLS still applies)&lt;br /&gt;
grant select, insert, update, delete on app.users            to web_anon;&lt;br /&gt;
grant select, insert, update, delete on app.groups           to web_anon;&lt;br /&gt;
grant select, insert, update, delete on app.group_members    to web_anon;&lt;br /&gt;
grant select, insert, update, delete on todo.todos           to web_anon;&lt;br /&gt;
grant select, insert, update, delete on todo.todo_group_shares to web_anon;&lt;br /&gt;
&lt;br /&gt;
-- Sequences (none; we use UUIDs)&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
# 7) (Nice to have) Triggers &amp;amp; helpers&lt;br /&gt;
&lt;br /&gt;
Auto-maintain `updated_at` on todos:&lt;br /&gt;
&lt;br /&gt;
```sql&lt;br /&gt;
create or replace function todo.bump_updated_at()&lt;br /&gt;
returns trigger language plpgsql as $$&lt;br /&gt;
begin&lt;br /&gt;
  new.updated_at := now();&lt;br /&gt;
  return new;&lt;br /&gt;
end$$;&lt;br /&gt;
&lt;br /&gt;
drop trigger if exists trg_todos_updated_at on todo.todos;&lt;br /&gt;
create trigger trg_todos_updated_at&lt;br /&gt;
before update on todo.todos&lt;br /&gt;
for each row execute function todo.bump_updated_at();&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
# 8) How to use (PostgREST / JWT)&lt;br /&gt;
&lt;br /&gt;
* PostgREST forwards JWT claims to `current_setting('request.jwt.claims', true)`.&lt;br /&gt;
* Your token must include a `sub`. Example payload:&lt;br /&gt;
&lt;br /&gt;
  ```json&lt;br /&gt;
  {&lt;br /&gt;
    &amp;quot;sub&amp;quot;: &amp;quot;c2d1ae8a-...-external-id-or-opaque&amp;quot;,&lt;br /&gt;
    &amp;quot;email&amp;quot;: &amp;quot;alice@example.com&amp;quot;,&lt;br /&gt;
    &amp;quot;name&amp;quot;: &amp;quot;Alice Example&amp;quot;,&lt;br /&gt;
    &amp;quot;exp&amp;quot;: 1900000000&lt;br /&gt;
  }&lt;br /&gt;
  ```&lt;br /&gt;
* First request from a `sub` will auto-provision a row in `app.users` and return a stable internal `id` UUID for joins.&lt;br /&gt;
&lt;br /&gt;
# 9) Notes &amp;amp; options&lt;br /&gt;
&lt;br /&gt;
* **Don’t use `sub` as PK**: different IdPs may change/format it differently. Keep `users.id` (UUID) as canonical, store `external_subject` (TEXT) as the mapping.&lt;br /&gt;
* **Group authority in DB**: we intentionally ignore any `groups` claim for authorization. JWT can lie or drift; DB is the source of truth.&lt;br /&gt;
* **Granularity**: the `todo_group_shares` flags (`can_read`, `can_write`, `can_del`) are easy to reason about. If you prefer, model a `perm` enum (`read`, `write`, `delete`, `share`) or a bitmask.&lt;br /&gt;
&lt;br /&gt;
---&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
</feed>