# 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 22–30) 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"
```
