Skip to content

Commit d3828d3

Browse files
johnckealyclaude
andcommitted
A profile row belongs to its account: say it with a foreign key
public.users.id IS the auth.users id, so the row is a child of the account. A foreign key with ON DELETE CASCADE replaces the on_auth_user_deleted trigger that deleted it by hand: it cannot be forgotten, it also rules out a profile for an account that does not exist, and it is one mechanism instead of two. Deleting an account still cascades on to devices. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 793ca90 commit d3828d3

2 files changed

Lines changed: 37 additions & 3 deletions

File tree

‎README.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,10 @@ await supabase.rpc('delete_own_account')
6161

6262
`delete_own_account()` is `SECURITY DEFINER`, takes no arguments, deletes only
6363
`auth.uid()` (the caller's own id from their session token), and may only be
64-
executed by signed-in users. The `on_auth_user_deleted` trigger removes the
65-
`public.users` row, which cascades to `devices`. See the migration
66-
`20260905120000_delete_own_account.sql` for the full reasoning.
64+
executed by signed-in users. Deleting the `auth.users` row cascades to the
65+
`public.users` row (the foreign key added in
66+
`20260917200000_users_follow_auth_users.sql`), and on to `devices`. See the
67+
migration `20260905120000_delete_own_account.sql` for the full reasoning.
6768

6869
The same deletion also exists as an API endpoint, `DELETE /auth/users/me`
6970
(`app/routes/auth_router.py`): the server deletes the caller with the
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
-- ============================================================================
2+
-- A PROFILE ROW BELONGS TO ITS ACCOUNT — SAID WITH A FOREIGN KEY
3+
-- ============================================================================
4+
-- `public.users.id` IS the `auth.users` id, so the row is a child of the
5+
-- account and should go when the account goes. Until now a trigger
6+
-- (`on_auth_user_deleted`) deleted it by hand. A foreign key says the same
7+
-- thing to the database itself, which is better in three ways:
8+
--
9+
-- * It cannot be forgotten. The trigger only ran for DELETE statements
10+
-- against auth.users; the key is enforced by the database.
11+
-- * It rules out the other orphan too: a profile row can no longer be
12+
-- inserted for an account that does not exist.
13+
-- * It is one mechanism instead of two, so nothing can half-happen.
14+
--
15+
-- Deleting an account still cascades on to `devices` (and any other table
16+
-- whose foreign key points at public.users), exactly as before.
17+
18+
-- Any profile row whose account is already gone would refuse the key. There
19+
-- should be none — the trigger removed them — but a database that was
20+
-- restored, or written to by hand, may have one. They belong to no account
21+
-- and nothing can reach them, so they go.
22+
DELETE FROM public.users u
23+
WHERE NOT EXISTS (SELECT 1 FROM auth.users a WHERE a.id = u.id);
24+
25+
ALTER TABLE public.users
26+
ADD CONSTRAINT users_auth_user_fkey
27+
FOREIGN KEY (id)
28+
REFERENCES auth.users(id)
29+
ON DELETE CASCADE;
30+
31+
-- The trigger and its function have nothing left to do.
32+
DROP TRIGGER IF EXISTS on_auth_user_deleted ON auth.users;
33+
DROP FUNCTION IF EXISTS public.handle_deleted_user();

0 commit comments

Comments
 (0)