Account Deletion Guide
This document provides the SQL queries and API commands needed to completely delete a user account from Tadoku.
Overview
Deleting an account involves:
- Deleting all user data from the immersion database (PostgreSQL)
- Deleting the identity from Kratos (identity management)
Prerequisites
- Database access to the immersion database
- Access to Kratos Admin API
- The user's account ID (UUID)
Step 1: Delete Data from Immersion Database
Run the following SQL transaction in the immersion database. Replace <ACCOUNT_ID> with the actual user UUID at the top.
-- Define the account ID to delete here
SET session.account_to_delete = '<ACCOUNT_ID>';
BEGIN;
-- Set the schema search path
SET LOCAL search_path TO data;
DO $$
DECLARE
v_account_id UUID := current_setting('session.account_to_delete')::UUID;
finished_contest_logs_count INTEGER;
BEGIN
-- Step 0: Check if user has logs attached to finished contests
-- If this query returns any rows, ABORT the deletion
SELECT COUNT(*)
INTO finished_contest_logs_count
FROM contest_logs cl
JOIN logs l ON cl.log_id = l.id
JOIN contests c ON cl.contest_id = c.id
WHERE l.user_id = v_account_id
AND c.contest_end < NOW()
AND c.deleted_at IS NULL;
IF finished_contest_logs_count > 0 THEN
RAISE EXCEPTION 'Cannot delete account: User has % logs attached to finished contests. Use account anonymization instead.', finished_contest_logs_count;
END IF;
-- Step 1: Delete contest_logs for user's logs
DELETE FROM contest_logs
WHERE log_id IN (SELECT id FROM logs WHERE user_id = v_account_id);
-- Step 2: Delete user's logs (hard delete)
DELETE FROM logs WHERE user_id = v_account_id;
-- Step 3: Delete user's contest registrations
DELETE FROM contest_registrations WHERE user_id = v_account_id;
-- Step 4: Anonymize contests owned by the user
-- (preserves contest data for other participants)
UPDATE contests
SET owner_user_id = '00000000-0000-0000-0000-000000000000',
owner_user_display_name = '[deleted user]'
WHERE owner_user_id = v_account_id;
-- Step 5: Delete user record
DELETE FROM users WHERE id = v_account_id;
END $$;
COMMIT;
-- Clean up the session variable
RESET session.account_to_delete;
Alternative: Delete Contests Instead of Anonymizing
If you prefer to completely delete contests owned by the user (note: this affects other participants), replace Step 4 with:
-- Step 4a: Delete contest_logs for user's contests
DELETE FROM contest_logs
WHERE contest_id IN (SELECT id FROM contests WHERE owner_user_id = '<ACCOUNT_ID>');
-- Step 4b: Delete registrations for user's contests
DELETE FROM contest_registrations
WHERE contest_id IN (SELECT id FROM contests WHERE owner_user_id = '<ACCOUNT_ID>');
-- Step 4c: Delete contests
DELETE FROM contests WHERE owner_user_id = '<ACCOUNT_ID>';
Step 2: Delete Identity from Kratos
Use the Kratos Admin API to delete the identity. Replace <KRATOS_ADMIN_URL> with your Kratos admin endpoint and <ACCOUNT_ID> with the user UUID.
curl -X DELETE \
'<KRATOS_ADMIN_URL>/admin/identities/<ACCOUNT_ID>' \
-H 'Accept: application/json'
Example
curl -X DELETE \
'http://localhost:4434/admin/identities/a1b2c3d4-e5f6-7890-abcd-ef1234567890' \
-H 'Accept: application/json'
Verification
After deletion, verify the account is gone:
-- Should return no rows
SELECT * FROM users WHERE id = '<ACCOUNT_ID>';
# Should return 404
curl -X GET \
'<KRATOS_ADMIN_URL>/admin/identities/<ACCOUNT_ID>' \
-H 'Accept: application/json'
Notes
-
Moderation audit logs are NOT deleted or anonymized by default. If needed, add:
DELETE FROM moderation_audit_log WHERE user_id = '<ACCOUNT_ID>'; -
Contest anonymization preserves contests for other participants while removing the deleted user's identity
-
Transaction safety: The SQL runs in a transaction, so if any step fails, all changes are rolled back
-
Kratos cleanup: If the Kratos deletion fails but database deletion succeeded, you can retry the curl command safely
Account Anonymization (For Users with Finished Contest Logs)
⚠️ TODO: Not Yet Implemented
If a user has logs attached to finished contests, complete deletion is not possible because it would corrupt historical contest data. Instead, the account must be anonymized.
When to Use Anonymization
The deletion query will abort with an error if the user has logs in finished contests. In this case, you must use account anonymization instead.
What Anonymization Should Do
The following functionality needs to be developed:
-
Anonymize user record:
- Set
display_nameto[deleted user <random-id>]to prevent display name collisions - Remove any PII fields if they exist
- Set
-
Anonymize logs:
- Keep logs intact (required for contest history)
- Consider adding a
anonymizedflag or field
-
Anonymize contest ownership (same as deletion):
- Set
owner_user_idto null UUID - Set
owner_user_display_nameto[deleted user]
- Set
-
Delete contest registrations:
- Remove from future/ongoing contests
- Keep registrations for finished contests (or anonymize if needed)
-
Delete Kratos identity:
- Prevent login while preserving data integrity
-
Mark account as deleted:
- Consider adding a
deleted_attimestamp to theuserstable - Ensure anonymized users can't be reactivated
- Consider adding a
Implementation Status
This query has not been implemented yet. When a user with finished contest logs needs to be deleted, you will need to:
- Develop the anonymization query following the guidelines above
- Test thoroughly to ensure contest data integrity is preserved
- Update this documentation with the final query
Troubleshooting
"Cannot delete account: User has logs attached to finished contests"
This error means the user has participated in contests that have already ended. Complete deletion would corrupt historical contest data.
Solution: Use the account anonymization approach instead (see section above - currently TODO).
Database deletion fails
Check for foreign key constraints or active connections. Ensure no other processes are modifying the user's data during deletion.
Kratos deletion fails with 404
The identity may already be deleted or never existed. This is safe to ignore if the database deletion succeeded.
Rollback needed
If you need to rollback the database transaction, run:
ROLLBACK;