Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ Next.js 16 App Router, TypeScript, Tailwind v4, Supabase. Multi-edition hackatho

- `create-next-app` fails in non-empty dirs. The project was scaffolded by hand (mirroring `superteam-maker`'s approach).
- **The DB was repurposed from `supabase-solana-lms`**, which dropped the `ALTER DEFAULT PRIVILEGES` rows that normally auto-grant `anon`/`authenticated`/`service_role` on every new table. Migrations 00009/00010 restore them explicitly. If you ever fork to a fresh Supabase project, those two migrations become no-ops but should still run for parity. Symptom of missing grants: every PostgREST call 403s with "permission denied for table X" *before* RLS even runs.
- **Default table privileges are trimmed (00069):** a new table gives `anon` SELECT and `authenticated` SELECT/INSERT/UPDATE, nothing more. A table with a DELETE policy needs `grant delete ... to authenticated` next to it. A new `public_*` view needs `revoke all ... from anon, authenticated` plus `grant select`: those views are owned by postgres, so an auto-updatable one otherwise accepts INSERT/UPDATE past RLS.
- Supabase Auth doesn't backfill the `users` mirror table for accounts that existed before the trigger was added. For a fresh project this won't bite, but if you re-apply migrations to a DB with pre-existing auth users:
```sql
INSERT INTO public.users (id, email)
Expand Down
27 changes: 27 additions & 0 deletions supabase/migrations/00069_least_privilege_table_grants.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-- Supabase's default ACL for tables created by postgres hands anon and
-- authenticated every privilege; 00025 only took insert/update/delete back
-- from anon. TRUNCATE ignores RLS, and REFERENCES, TRIGGER and MAINTAIN are
-- owner tools no request needs.
revoke truncate, references, trigger, maintain on all tables in schema public from anon, authenticated;

-- RLS allows a DELETE only where a DELETE policy exists, and the one such
-- policy is team_members_leader_delete. Every other delete in the app goes
-- through a SECURITY DEFINER RPC or the service role.
revoke delete on all tables in schema public from anon, authenticated;
grant delete on public.team_members to authenticated;

-- The public_* views are owned by postgres, so a write through them skips
-- RLS on the base table. public_profiles is auto-updatable: with the
-- insert/update/delete the default ACL gave authenticated, any signed-in
-- user could rewrite or delete another participant's users row.
revoke all on public.public_profiles, public.public_submissions,
public.public_team_members, public.public_schedule from anon, authenticated;
grant select on public.public_profiles, public.public_submissions,
public.public_team_members, public.public_schedule to anon, authenticated;

-- New tables stop inheriting the same set. A table that needs DELETE grants
-- it next to the policy that allows it.
alter default privileges in schema public
revoke truncate, references, trigger, maintain on tables from anon, authenticated;
alter default privileges in schema public
revoke delete on tables from anon, authenticated;
Loading