docs: add account deletion implementation plan

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Arthur Belleville 2026-04-29 15:37:14 +02:00
parent 03e1f335bc
commit a8bae0c1c3
No known key found for this signature in database

View file

@ -0,0 +1,320 @@
# Account Deletion Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add account deletion to the Expo app — a "Danger Zone" button in Settings that calls a new `DELETE /users/me` API endpoint, soft-deletes the profile (and org if sole member), then hard-deletes the Supabase auth user.
**Architecture:** The API endpoint sets `deleted_at` on `profiles` (and `organizations` if the user is the sole member), then calls `supabase.auth.admin.deleteUser` to revoke access. The Expo app shows a native `Alert` confirmation, calls the endpoint with a Bearer token, then calls `signOut()` to clear local state. No cron purge — orphaned data stays in the DB.
**Tech Stack:** Hono (API), Supabase (auth + DB), React Native `Alert`, Zustand auth store, Expo Router
---
## File Map
| Action | File |
|--------|------|
| Modify | `apps/api/src/routers/user.ts` |
| Modify | `apps/api/src/__tests__/routes/user.test.ts` |
| Modify | `xtablo-expo/app/(app)/(tabs)/settings.tsx` |
---
### Task 1: Database Migrations
**Files:**
- Modify: Supabase database (via dashboard SQL editor or Supabase CLI)
- [ ] **Step 1: Run migration SQL in Supabase dashboard**
Open the Supabase project → SQL Editor → run:
```sql
-- Add deleted_at to profiles
ALTER TABLE profiles ADD COLUMN IF NOT EXISTS deleted_at timestamptz DEFAULT NULL;
-- Add deleted_at to organizations
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS deleted_at timestamptz DEFAULT NULL;
```
- [ ] **Step 2: Verify columns exist**
In Supabase SQL Editor:
```sql
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_name IN ('profiles', 'organizations')
AND column_name = 'deleted_at';
```
Expected: 2 rows returned, both with `data_type = 'timestamp with time zone'` and `is_nullable = YES`.
- [ ] **Step 3: Regenerate TypeScript types**
```bash
cd /path/to/repo
npx supabase gen types typescript --project-id <your-project-id> > packages/shared-types/src/database.types.ts
```
Verify `profiles.Row` now has `deleted_at: string | null`.
- [ ] **Step 4: Commit**
```bash
git add packages/shared-types/src/database.types.ts
git commit -m "chore: add deleted_at to profiles and organizations, regenerate types"
```
---
### Task 2: API — `DELETE /users/me` Endpoint (TDD)
**Files:**
- Modify: `apps/api/src/__tests__/routes/user.test.ts`
- Modify: `apps/api/src/routers/user.ts`
- [ ] **Step 1: Write the failing test**
Open `apps/api/src/__tests__/routes/user.test.ts`. Add a new `describe` block after the existing ones:
```typescript
describe("DELETE /me - Delete Account", () => {
it("should return 401 when unauthenticated", async () => {
const res = await client.users.me.$delete({});
expect(res.status).toBe(401);
});
it("should delete the authenticated user's account", async () => {
const res = await client.users.me.$delete(
{},
{
headers: {
Authorization: `Bearer ${ownerUser.accessToken}`,
"Content-Type": "application/json",
},
}
);
expect(res.status).toBe(200);
const data = await res.json();
expect(data).toEqual({ message: "Account deleted successfully" });
});
});
```
- [ ] **Step 2: Run the test to verify it fails**
```bash
cd apps/api
pnpm test --reporter=verbose src/__tests__/routes/user.test.ts
```
Expected: the new tests fail with `404` or a method-not-found error (route doesn't exist yet). The existing tests should still pass.
- [ ] **Step 3: Implement the `deleteMe` handler in `apps/api/src/routers/user.ts`**
Add this handler after the `removeOrganizationMember` handler (around line 803, before `export const getUserRouter`):
```typescript
const deleteMe = factory.createHandlers(async (c) => {
const user = c.get("user");
const supabase = c.get("supabase");
// Fetch profile to get organization_id
const { data: profile, error: profileError } = await supabase
.from("profiles")
.select("organization_id")
.eq("id", user.id)
.single();
if (profileError || !profile) {
return c.json({ error: "User not found" }, 404);
}
// If user is sole org member, soft-delete the org too
if (profile.organization_id) {
const { count } = await supabase
.from("profiles")
.select("id", { count: "exact", head: true })
.eq("organization_id", profile.organization_id);
if ((count ?? 0) === 1) {
await supabase
.from("organizations")
.update({ deleted_at: new Date().toISOString() })
.eq("id", profile.organization_id);
}
}
// Soft-delete the profile
await supabase
.from("profiles")
.update({ deleted_at: new Date().toISOString() })
.eq("id", user.id);
// Hard-delete the Supabase auth user (revokes all sessions immediately)
const { error: authDeleteError } = await supabase.auth.admin.deleteUser(user.id);
if (authDeleteError) {
// Roll back the soft delete
await supabase
.from("profiles")
.update({ deleted_at: null })
.eq("id", user.id);
return c.json({ error: "Failed to delete account" }, 500);
}
return c.json({ message: "Account deleted successfully" });
});
```
- [ ] **Step 4: Register the route in `getUserRouter`**
In the `getUserRouter` function (around line 804), add the new route:
```typescript
export const getUserRouter = () => {
const userRouter = new Hono();
userRouter.get("/me", ...getMe);
userRouter.delete("/me", ...deleteMe); // ← add this line
userRouter.post("/mark-temporary", ...markTemporary);
userRouter.post("/profile/avatar", ...uploadAvatar);
userRouter.delete("/profile/avatar", ...deleteAvatar);
userRouter.get("/organization", ...getOrganization);
userRouter.patch("/organization", ...updateOrganization);
userRouter.post("/organization/invite", ...inviteToOrganization);
userRouter.delete("/organization/members/:memberId", ...removeOrganizationMember);
return userRouter;
};
```
- [ ] **Step 5: Run the tests to verify they pass**
```bash
cd apps/api
pnpm test --reporter=verbose src/__tests__/routes/user.test.ts
```
Expected: all tests pass, including the two new DELETE ones.
- [ ] **Step 6: Commit**
```bash
git add apps/api/src/routers/user.ts apps/api/src/__tests__/routes/user.test.ts
git commit -m "feat(api): add DELETE /users/me account deletion endpoint"
```
---
### Task 3: Expo — Danger Zone UI in Settings
**Files:**
- Modify: `xtablo-expo/app/(app)/(tabs)/settings.tsx`
- [ ] **Step 1: Add the `Trash2` icon import**
In `settings.tsx`, find the existing lucide import block (lines 2230) and add `Trash2`:
```typescript
import {
User,
Bell,
Moon,
Shield,
HelpCircle,
Info,
MessageSquare,
LogOut,
ChevronRight,
Smartphone,
Globe,
Lock,
Heart,
Trash2, // ← add this
} from "lucide-react-native";
```
- [ ] **Step 2: Add the `handleDeleteAccount` function**
In `settings.tsx`, add this function after `handleRateApp` (around line 91):
```typescript
const handleDeleteAccount = () => {
Alert.alert(
"Supprimer le compte",
"Cette action est irréversible. Votre compte et toutes vos données seront définitivement supprimés. Êtes-vous sûr de vouloir continuer ?",
[
{ text: "Annuler", style: "cancel" },
{
text: "Supprimer mon compte",
style: "destructive",
onPress: async () => {
try {
const session = useAuthStore.getState().session;
await api.delete("/api/v1/users/me", {
headers: { Authorization: `Bearer ${session?.access_token}` },
});
await signOut();
} catch {
Alert.alert(
"Erreur",
"Une erreur est survenue lors de la suppression de votre compte. Veuillez réessayer."
);
}
},
},
]
);
};
```
- [ ] **Step 3: Add the Danger Zone section above the Sign Out button**
In `settings.tsx`, find the `{/* Sign Out Section */}` comment (around line 315). Insert the Danger Zone section immediately before it:
```tsx
{/* Danger Zone Section */}
{renderSettingsSection(
"Zone de danger",
<>
{renderSettingsItem(
<Trash2 size={20} color="#ef4444" />,
"Supprimer le compte",
"Supprimer définitivement votre compte et vos données",
handleDeleteAccount,
undefined,
true
)}
</>
)}
{/* Sign Out Section */}
```
- [ ] **Step 4: Verify the import of `useAuthStore` is used for `session`**
The `handleDeleteAccount` function calls `useAuthStore.getState().session`. Confirm `useAuthStore` is already imported at the top of the file:
```typescript
import { useAuthStore } from "@/stores/auth";
```
It already is (line 14) — no change needed.
- [ ] **Step 5: Run the Expo app and test the flow manually**
```bash
cd xtablo-expo
npx expo start
```
Navigate to Settings → scroll to "Zone de danger" → tap "Supprimer le compte" → confirm the Alert appears with the warning text and a "Supprimer mon compte" destructive button → tap Cancel to verify it dismisses.
- [ ] **Step 6: Commit**
```bash
git add xtablo-expo/app/(app)/(tabs)/settings.tsx
git commit -m "feat(expo): add account deletion in settings danger zone"
```