docs(adr): file de suppression — deux conceptions mortes avant la bonne (ADR-024)#279
Conversation
L'étape 3b doit brancher un exécuteur sur une file de suppression que rien ne
vide, pendant que l'écran de statut affiche à l'utilisateur une date et un
décompte de jours. Ce n'est pas une omission, c'est une affirmation inexacte
faite à la personne concernée (art. 12(1)) sur un droit garanti par l'art. 17.
Brancher cet exécuteur impose des choix de schéma qui ne se défont pas :
statut supplémentaire, colonne de verrou, index unique partiel, fonction SQL,
et réécriture de deux politiques RLS. ADR-023 ne couvrait que le délai de
grâce. D'où celui-ci — et, par doctrine, aucune ligne de code applicatif ici.
Deux conceptions ont été écrites puis abandonnées, chacune tuée par une
mesure. Elles sont consignées parce que rien n'invite plus sûrement à les
réécrire que leur absence :
1. Une fonction SECURITY DEFINER atomique. FORCE ROW LEVEL SECURITY
s'applique au propriétaire de la table : possédée par postgres, elle
écrirait zéro ligne SANS ERREUR si le postgres hébergé n'a pas
BYPASSRLS — ce qu'on ne peut pas mesurer. Compte supprimé, IP et
user-agent conservés, plus aucune clé pour les retrouver.
2. La même en SECURITY INVOKER appelée par service_role. Mesuré :
service_role n'a AUCUN privilège sur auth.users ni sur
auth.audit_log_entries. Aucune fonction SQL appelée par l'app ne peut
atteindre le schéma auth.
Conclusion qui contraint tout le reste : l'atomicité SQL de bout en bout est
impossible par toute conception. Ce n'est pas une limite de notre code, c'est
la frontière entre PostgREST et GoTrue.
La garantie devient donc l'idempotence et la reprise. Il n'y a qu'une
instruction irréversible ; la précéder d'un nettoyage rejouable suffit, et
aucun privilège nouveau n'est demandé — c'est ce qui rend cette conception
défendable là où les deux autres reposaient sur une hypothèse invérifiable.
Trois corollaires contre-intuitifs, écrits pour ne pas être redécouverts :
completed/completed_at sont inatteignables (la ligne cascade avec le compte),
un « user not found » compte comme un succès, et une pseudonymisation touchant
zéro ligne aussi — l'inverse gèlerait la file pour tout compte sans événement
d'audit, soit exactement la panne muette que ce chantier corrige.
Le verrou anti-double-suppression repose sur une nouvelle colonne claimed_at,
jamais sur requested_at : une ligne n'étant réclamable que 14 jours après sa
demande, un test sur requested_at serait toujours vrai et remettrait en file
les lignes qu'un run concurrent traite.
deletion_self_insert est supprimée plutôt que durcie : vérifié, aucune
insertion client n'existe dans src/, la politique accorde donc une capacité
que le produit n'utilise pas — et c'est la capacité, pas sa date, qui est le
vecteur de vol de session.
Livraison en deux PR : A inerte (rien ne peut détruire), B armement. La revue
de B ne portera alors que sur une question, est-ce que ça peut partir quand il
ne faut pas.
Le plan d'exécution correspondant part avec, dans docs/plans/.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
Sorry @thierryvm, you have reached your weekly rate limit of 500000 diff characters.
Please try again later or upgrade to continue using Sourcery
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Guide du relecteurAjoute l’ADR-024 documentant l’architecture finale et les contraintes de la file d’attente de suppression de compte, ainsi qu’un plan d’exécution détaillé pour l’implémenter en deux PR (A : câblage inert de la file, B : activation via cron), incluant les changements de schéma, invariants, tests et vérifications en production — sans encore modifier le code applicatif. Diagramme de séquence pour le nouveau traitement de la file d’attente de suppression de comptesequenceDiagram
actor Cron
participant Route_api_cron_gdpr as api_cron_gdpr_route
participant PostgREST_service_role as PostgREST_service_role
participant claim_pending_deletions as claim_pending_deletions
participant DeletionCore as deletion_core_ts
participant GoTrue as GoTrue_auth_admin
participant DB as Postgres_public
Cron->>Route_api_cron_gdpr: GET /api/cron/gdpr
Route_api_cron_gdpr->>Route_api_cron_gdpr: verifyCronSecret
Route_api_cron_gdpr->>PostgREST_service_role: claimPendingDeletions(25)
PostgREST_service_role->>claim_pending_deletions: call(batch_size)
claim_pending_deletions->>DB: update deletion_requests reset stale processing
claim_pending_deletions->>DB: update deletion_requests set status=processing, claimed_at=now()
claim_pending_deletions-->>PostgREST_service_role: return request_id, target_user_id*
PostgREST_service_role-->>Route_api_cron_gdpr: claimed deletions
loop for each request
Route_api_cron_gdpr->>DeletionCore: executeDeletionWith(client, userId)
DeletionCore->>DB: pseudonymiseAuditLog(client, userId)
alt [pseudonymiseAuditLog error]
DeletionCore-->>Route_api_cron_gdpr: error
else [pseudonymiseAuditLog ok]
DeletionCore->>GoTrue: auth.admin.deleteUser(userId)
GoTrue-->>DeletionCore: result (including user not found)
DeletionCore-->>Route_api_cron_gdpr: success
end
end
Route_api_cron_gdpr->>PostgREST_service_role: purge_audit_log_older_than_12_months()
Route_api_cron_gdpr-->>Cron: JSON {claimed, deleted, failed, purged, capped}
Diagramme de relations entre entités pour le schéma mis à jour de la file de suppressionerDiagram
auth_users {
uuid id
}
public_users {
uuid id
uuid auth_user_id
}
deletion_requests {
uuid id
uuid user_id
text status
timestamptz requested_at
timestamptz scheduled_for
timestamptz claimed_at
}
audit_log {
uuid id
uuid user_id
inet ip_address
text user_agent
}
auth_users ||--o{ public_users : auth_cascade
public_users ||--o{ deletion_requests : user_cascade
public_users ||--o{ audit_log : on_delete_set_null
%% Unique partial index conceptually: one active request per user
deletion_requests }o..o{ deletion_requests : one_active_pending_or_processing_per_user
Modifications au niveau des fichiers
Conseils et commandesInteragir avec Sourcery
Personnaliser votre expérienceAccédez à votre dashboard pour :
Obtenir de l’aide
Original review guide in EnglishReviewer's GuideAdds ADR-024 documenting the final architecture and constraints for the account deletion queue, and a detailed execution plan for implementing it in two PRs (A: inert queue wiring, B: cron arming), including schema changes, invariants, tests, and production checks—without changing any application code yet. Sequence diagram for the new account deletion queue processingsequenceDiagram
actor Cron
participant Route_api_cron_gdpr as api_cron_gdpr_route
participant PostgREST_service_role as PostgREST_service_role
participant claim_pending_deletions as claim_pending_deletions
participant DeletionCore as deletion_core_ts
participant GoTrue as GoTrue_auth_admin
participant DB as Postgres_public
Cron->>Route_api_cron_gdpr: GET /api/cron/gdpr
Route_api_cron_gdpr->>Route_api_cron_gdpr: verifyCronSecret
Route_api_cron_gdpr->>PostgREST_service_role: claimPendingDeletions(25)
PostgREST_service_role->>claim_pending_deletions: call(batch_size)
claim_pending_deletions->>DB: update deletion_requests reset stale processing
claim_pending_deletions->>DB: update deletion_requests set status=processing, claimed_at=now()
claim_pending_deletions-->>PostgREST_service_role: return request_id, target_user_id*
PostgREST_service_role-->>Route_api_cron_gdpr: claimed deletions
loop for each request
Route_api_cron_gdpr->>DeletionCore: executeDeletionWith(client, userId)
DeletionCore->>DB: pseudonymiseAuditLog(client, userId)
alt [pseudonymiseAuditLog error]
DeletionCore-->>Route_api_cron_gdpr: error
else [pseudonymiseAuditLog ok]
DeletionCore->>GoTrue: auth.admin.deleteUser(userId)
GoTrue-->>DeletionCore: result (including user not found)
DeletionCore-->>Route_api_cron_gdpr: success
end
end
Route_api_cron_gdpr->>PostgREST_service_role: purge_audit_log_older_than_12_months()
Route_api_cron_gdpr-->>Cron: JSON {claimed, deleted, failed, purged, capped}
Entity relationship diagram for the updated deletion queue schemaerDiagram
auth_users {
uuid id
}
public_users {
uuid id
uuid auth_user_id
}
deletion_requests {
uuid id
uuid user_id
text status
timestamptz requested_at
timestamptz scheduled_for
timestamptz claimed_at
}
audit_log {
uuid id
uuid user_id
inet ip_address
text user_agent
}
auth_users ||--o{ public_users : auth_cascade
public_users ||--o{ deletion_requests : user_cascade
public_users ||--o{ audit_log : on_delete_set_null
%% Unique partial index conceptually: one active request per user
deletion_requests }o..o{ deletion_requests : one_active_pending_or_processing_per_user
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
#280) ## Pourquoi maintenant Règle durable du projet : **le ROADMAP se remet à jour avant d'ouvrir la branche suivante, pas après.** La branche suivante est PR-A de l'étape 3b. Deux écarts à solder d'abord. ## Écart 1 — l'étape 3 était annoncée « suivante » Elle est en cours depuis deux jours, et son détail vaut d'être lisible. L'exécution des spécifications a montré que le préalable n'était pas la file de suppression, mais **ce sur quoi elle repose** : le journal d'audit n'enregistrait rien depuis avril, et trois affirmations publiques étaient inexactes. Six lots livrés (#273 → #279), deux restants (PR-A inerte, PR-B armement), et un écart art. 17 mesuré qui part en session dédiée (#278). Le **verrou de PR-B** est désormais écrit dans le ROADMAP : trois lectures production, dont une NO-GO — toute la conception repose sur un chemin PostgREST `service_role` jamais re-vérifié en production depuis #273. ## Écart 2 — une dette réglée encore listée comme ouverte « Angle mort du préflight comptes » figurait dans les dettes ouvertes alors que #276 l'a comblé ce matin. Un ROADMAP qui garde une dette réglée est aussi trompeur qu'un qui en oublie une : il fait perdre du temps à qui la reprend. ## Portée Un seul fichier, markdown. Aucun code. 🤖 Generated with [Claude Code](https://claude.com/claude-code) ## Summary by Sourcery Mettre à jour le ROADMAP pour refléter l’état actuel et la répartition détaillée de l’étape 3, ainsi que la résolution récente de la dette. Documentation : - Ajouter une sous-section détaillée documentant l’avancement de l’étape 3, les lots livrés, les PR restantes et les contraintes associées en matière d’audit/journalisation. - Marquer la dette « Angle mort du préflight comptes » comme résolue, avec une explication mise à jour du nouveau comportement de vérification pour les comptes Supabase et Vercel. <details> <summary>Original summary in English</summary> ## Summary by Sourcery Update the ROADMAP to reflect the current status and detailed breakdown of step 3 and recent debt resolution. Documentation: - Add a detailed sub-section documenting step 3 progress, delivered lots, remaining PRs, and associated audit/logging constraints. - Mark the 'Angle mort du préflight comptes' debt as resolved with an updated explanation of the new verification behavior for Supabase and Vercel accounts. </details> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Pourquoi cet ADR existe
L'étape 3b doit brancher un exécuteur sur une file de suppression que rien ne vide,
pendant que
/app/settings/deletion-statusaffiche à l'utilisateur une date et un décomptede jours restants. Ce n'est pas une omission : c'est une affirmation inexacte faite à la
personne concernée (art. 12(1)) sur un droit garanti par l'art. 17.
Brancher cet exécuteur impose des choix de schéma qui ne se défont pas : statut
supplémentaire, colonne de verrou, index unique partiel, fonction SQL, réécriture de deux
politiques RLS. ADR-023 ne couvrait que le délai de grâce. Doctrine projet : décision en
session N, implémentation en session N+1 — donc zéro ligne de code applicatif ici.
Deux conceptions mortes, consignées exprès
Rien n'invite plus sûrement à réécrire une mauvaise idée que son absence du dossier.
Conception 1 — fonction
SECURITY DEFINERatomique. Rejetée.FORCE ROW LEVEL SECURITYs'applique au propriétaire de la table. Possédée parpostgres, la fonction écrirait zéro ligne sans lever d'erreur si lepostgreshébergé n'a pas
BYPASSRLS— ce qu'on ne peut pas mesurer. Compte supprimé, IP etuser-agent conservés, plus aucune clé de jointure pour les retrouver.
Conception 2 — la même en
SECURITY INVOKERappelée parservice_role. Rejetée aussi.Mesuré :
service_rolen'a aucun privilège surauth.usersni surauth.audit_log_entries.Conséquence : l'atomicité SQL de bout en bout est impossible par toute conception.
Ce n'est pas une limite de notre code, c'est la frontière entre PostgREST et GoTrue.
La décision
La garantie devient l'idempotence et la reprise, pas l'atomicité. Il n'y a qu'une
instruction irréversible ; la précéder d'un nettoyage rejouable suffit. Et surtout :
aucun privilège nouveau n'est demandé — c'est ce qui rend cette conception défendable
là où les deux autres reposaient sur une hypothèse invérifiable.
Trois corollaires contre-intuitifs, écrits pour ne pas être redécouverts :
completed/completed_atsont inatteignables — la ligne cascade avec le compte.deleteUser« user not found » compte comme un succès, sinon la ligne devient unepilule empoisonnée réclamée et échouée chaque jour, pour toujours.
pour tout compte sans événement d'audit — exactement la panne muette qu'on corrige.
Le verrou anti-double-suppression repose sur une nouvelle colonne
claimed_at, jamaissur
requested_at: une ligne n'étant réclamable que 14 jours après sa demande, un testsur
requested_atserait toujours vrai et remettrait en file les lignes qu'uneexécution concurrente est en train de traiter.
deletion_self_insertest supprimée plutôt que durcie. Vérifié : aucune insertionclient n'existe dans
src/. La politique accorde une capacité que le produit n'utilisepas — et c'est la capacité, pas sa date, qui est le vecteur de vol de session.
Livraison en deux PR : A inerte (rien ne peut détruire), B armement. La revue de B ne
portera alors que sur une question : est-ce que ça peut partir quand il ne faut pas ?
Contenu
docs/adr/ADR-024-…— la décision, ce qui est écarté, et ce qui restera non prouvédocs/plans/step-3b-deletion-queue.md— le plan d'exécution pour la session suivanteCe qui reste à faire côté @Thierry
Trois lectures SQL en production, aucune écriture (détail dans le plan). La troisième
est un NO-GO de PR-B : toute la conception repose sur un chemin PostgREST
service_rolejamais re-vérifié en production depuis le correctif #273.
Portée
Markdown uniquement, aucun code applicatif.
plan-reviewera fait trois tours sur cedossier et a bloqué à chaque fois quelque chose de réel — dont, au dernier tour, le fait
que cette décision de schéma ne pouvait pas partir dans la même session que son
implémentation.
🤖 Generated with Claude Code
Résumé par Sourcery
Documenter la conception de la file d’attente de suppression de compte et le plan de mise en œuvre, en séparant les modifications de schéma et d’orchestration (PR-A) du câblage du cron et de la modification de la période de grâce (PR-B).
Améliorations :
Documentation :
Original summary in English
Summary by Sourcery
Document the account deletion queue design and implementation plan, separating schema and orchestration changes (PR-A) from cron wiring and grace-period change (PR-B).
Enhancements:
Documentation: