# Phase 1 — Better Auth `organization` foundation 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:** Introduire le modèle multi-tenant en DB via le plugin `organization` de Better Auth, créer une Personal Org automatiquement à chaque signup, et backfill une Personal Org pour chaque user existant.

**Architecture:** On ajoute le plugin officiel `organization()` à la config Better Auth existante (`lib/auth.ts`). Le plugin crée les tables `organization` / `member` / `invitation` et ajoute `active_organization_id` à `session`. Un helper `createPersonalOrg(user)` dans `lib/onboarding.ts` est appelé depuis le hook `databaseHooks.user.create.after` pour les nouveaux signups, et depuis un script one-shot `scripts/backfill-orgs.ts` pour les users existants.

**Tech Stack:** Better Auth 1.6+ (plugin `organization`), Drizzle ORM 0.45, drizzle-kit 0.31, Postgres (Docker local), tsx (scripts), nanoid (IDs), Zod (env).

**Spec source:** `docs/superpowers/specs/2026-05-25-org-roles-stripe-billing-design.md` §4.1, §4.2, §3.4, §4.6 steps 1-2 et 5 (partie orgs only).

**Hors-scope de cette phase (déféré aux phases 2+) :**

- Migration `orgId` sur `es_site` / `es_site_test` (Phase 2)
- `es_org_subscription`, `es_stripe_event`, Stripe (Phases 5-6)
- UI org switcher, settings, RBAC (Phases 3-4)
- Email d'invitation (Phase 4)
- Vitest infra (peut être ajouté dans une phase ultérieure si besoin de tests unit)

---

## File Structure

| Path                           | Status    | Responsibility                                                                                                                                  |
| ------------------------------ | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `lib/db/schema.ts`             | modify    | Ajout des tables `organization`, `member`, `invitation` ; ajout de la colonne `active_organization_id` sur `session`.                           |
| `lib/auth.ts`                  | modify    | Ajout du plugin `organization()` à la liste des plugins ; extension du hook `databaseHooks.user.create.after` pour appeler `createPersonalOrg`. |
| `lib/onboarding.ts`            | create    | Helper `createPersonalOrg(user)` : crée une org `Personal — {email}` + un member owner. Réutilisé par le hook ET le script de backfill.         |
| `drizzle/000X_*.sql`           | generated | Migration générée par `pnpm db:generate` (auto).                                                                                                |
| `scripts/backfill-orgs.ts`     | create    | Script one-shot : pour chaque user sans membership, créer une Personal Org. Idempotent.                                                         |
| `scripts/verify-org-signup.ts` | create    | Smoke test : signup un user éphémère + vérifier qu'une org + member row existent + cleanup.                                                     |
| `package.json`                 | modify    | Ajout des scripts `db:backfill-orgs` et `verify:org-signup`.                                                                                    |

---

## Task 1: Préparer la DB locale et vérifier l'état de départ

**Files:**

- Read: `docker-compose.yml`, `.env.local`

- [ ] **Step 1.1: Démarrer Postgres en local**

Run:

```bash
pnpm db:up
```

Expected: container `postgres` up, healthcheck OK. Vérifier avec `docker compose ps`.

- [ ] **Step 1.2: Vérifier que les migrations existantes ont été appliquées**

Run:

```bash
pnpm db:migrate
```

Expected: "No changes detected" OU les migrations 0000-0003 s'appliquent sans erreur.

- [ ] **Step 1.3: Vérifier qu'il existe au moins 1 user (pour tester le backfill plus tard)**

Run:

```bash
docker compose exec postgres psql -U postgres -d eleva_speed -c 'SELECT id, email FROM "user";'
```

Expected: au moins 1 row (probablement `bastien@elevaseo.com`). Si vide, lancer `pnpm db:seed`.

- [ ] **Step 1.4: Capturer l'état "before" du schema**

Run:

```bash
docker compose exec postgres psql -U postgres -d eleva_speed -c '\dt'
```

Expected: voir les tables `user`, `session`, `account`, `verification`, `es_site`, `es_site_test`, `es_lead`. **Pas** de `organization` / `member` / `invitation`.

---

## Task 2: Ajouter les tables du plugin `organization` au schema Drizzle

**Files:**

- Modify: `lib/db/schema.ts` (ajouter à la fin du bloc Better Auth, avant la section Sites)

- [ ] **Step 2.1: Ajouter la colonne `active_organization_id` à la table `session`**

Dans `lib/db/schema.ts`, modifier la définition de `session` pour ajouter une nouvelle colonne juste avant la fermeture des accolades (après `impersonatedBy`) :

```ts
export const session = pgTable("session", {
  id: text("id").primaryKey(),
  expiresAt: timestamp("expires_at", { withTimezone: true, mode: "date" }).notNull(),
  token: text("token").notNull().unique(),
  ipAddress: text("ip_address"),
  userAgent: text("user_agent"),
  userId: text("user_id")
    .notNull()
    .references(() => user.id, { onDelete: "cascade" }),
  createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).notNull().defaultNow(),
  updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }).notNull().defaultNow(),
  // Better Auth admin plugin (impersonation)
  impersonatedBy: text("impersonated_by"),
  // Better Auth organization plugin
  activeOrganizationId: text("active_organization_id"),
});
```

- [ ] **Step 2.2: Ajouter la table `organization` après le bloc `verification`**

Dans `lib/db/schema.ts`, juste après la définition de `verification` et avant le commentaire `/* -------------------------------- Sites -------------------------------- */`, ajouter :

```ts
/* ------------------------ Better Auth — Organization ------------------------ */
/* Tables required by better-auth's `organization` plugin. Singular, snake_case. */

export const organization = pgTable("organization", {
  id: text("id").primaryKey(),
  name: text("name").notNull(),
  slug: text("slug").notNull().unique(),
  logo: text("logo"),
  metadata: text("metadata"),
  createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).notNull().defaultNow(),
});
```

- [ ] **Step 2.3: Ajouter la table `member`**

Juste après `organization` :

```ts
export const member = pgTable(
  "member",
  {
    id: text("id").primaryKey(),
    organizationId: text("organization_id")
      .notNull()
      .references(() => organization.id, { onDelete: "cascade" }),
    userId: text("user_id")
      .notNull()
      .references(() => user.id, { onDelete: "cascade" }),
    role: text("role").notNull(), // "owner" | "admin" | "member"
    createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).notNull().defaultNow(),
  },
  (t) => [index("idx_member_org").on(t.organizationId), index("idx_member_user").on(t.userId)],
);
```

- [ ] **Step 2.4: Ajouter la table `invitation`**

Juste après `member` :

```ts
export const invitation = pgTable(
  "invitation",
  {
    id: text("id").primaryKey(),
    organizationId: text("organization_id")
      .notNull()
      .references(() => organization.id, { onDelete: "cascade" }),
    email: text("email").notNull(),
    role: text("role").notNull(), // "admin" | "member"
    status: text("status").notNull(), // "pending" | "accepted" | "rejected" | "expired"
    inviterId: text("inviter_id")
      .notNull()
      .references(() => user.id),
    expiresAt: timestamp("expires_at", { withTimezone: true, mode: "date" }).notNull(),
  },
  (t) => [index("idx_invitation_org").on(t.organizationId)],
);
```

- [ ] **Step 2.5: Vérifier que le fichier compile**

Run:

```bash
pnpm typecheck
```

Expected: PASS (0 errors). Si erreur d'import manquant, ajouter ce qui manque en haut du fichier (les imports `pgTable`, `text`, `timestamp`, `index` sont déjà présents).

---

## Task 3: Générer et appliquer la migration Drizzle

**Files:**

- Generated: `drizzle/000X_*.sql` (numéro auto)

- [ ] **Step 3.1: Générer la migration**

Run:

```bash
pnpm db:generate
```

Expected: nouveau fichier `drizzle/0004_*.sql` créé contenant `CREATE TABLE organization`, `CREATE TABLE member`, `CREATE TABLE invitation`, `ALTER TABLE session ADD COLUMN active_organization_id`.

- [ ] **Step 3.2: Inspecter la migration générée**

Run:

```bash
ls -la drizzle/ | tail -3
```

Puis lire le fichier `.sql` le plus récent. Vérifier :

- ✅ 3 `CREATE TABLE` (organization, member, invitation)
- ✅ 1 `ALTER TABLE session ADD COLUMN active_organization_id`
- ✅ Les `FOREIGN KEY` sur `member.organization_id`, `member.user_id`, `invitation.organization_id`, `invitation.inviter_id`
- ✅ Les 3 indexes (`idx_member_org`, `idx_member_user`, `idx_invitation_org`)
- ❌ Aucun `DROP` ni mutation sur les tables existantes (sauf l'ALTER de session)

Si quelque chose manque ou un DROP intempestif apparaît, **stop et review le schema avant d'appliquer**.

- [ ] **Step 3.3: Appliquer la migration**

Run:

```bash
pnpm db:migrate
```

Expected: log "applying migration 0004\_\*.sql" + "no errors".

- [ ] **Step 3.4: Vérifier l'état post-migration de la DB**

Run:

```bash
docker compose exec postgres psql -U postgres -d eleva_speed -c '\dt'
```

Expected: les tables `organization`, `member`, `invitation` sont présentes.

Puis :

```bash
docker compose exec postgres psql -U postgres -d eleva_speed -c '\d session' | grep active_organization_id
```

Expected: une ligne mentionnant `active_organization_id | text`.

- [ ] **Step 3.5: Commit le schema + la migration**

Run:

```bash
git add lib/db/schema.ts drizzle/0004_*.sql drizzle/meta/
git commit -m "feat(db): add Better Auth organization tables (org, member, invitation)"
```

---

## Task 4: Activer le plugin `organization` dans `lib/auth.ts`

**Files:**

- Modify: `lib/auth.ts`

- [ ] **Step 4.1: Importer `organization` depuis Better Auth plugins**

Dans `lib/auth.ts`, modifier l'import existant :

```ts
import { admin, organization } from "better-auth/plugins";
```

(Remplace `import { admin } from "better-auth/plugins";`)

- [ ] **Step 4.2: Ajouter le plugin à la liste**

Modifier la ligne `plugins: [admin(), nextCookies()],` pour :

```ts
plugins: [admin(), organization(), nextCookies()],
```

Note : on ne configure pas encore `sendInvitationEmail` ni les permissions custom — défauts Better Auth pour Phase 1, on durcira en Phase 4.

- [ ] **Step 4.3: Vérifier que ça type-check**

Run:

```bash
pnpm typecheck
```

Expected: PASS. Si erreur "type Session is incompatible", c'est probablement OK — le plugin étend les types de session ; vérifier que `lib/auth-guards.ts` (qui utilise `Session`) reste valide. Si besoin, on ajustera à la Phase 3.

- [ ] **Step 4.4: Démarrer le dev server et vérifier que Better Auth boot sans erreur**

Run:

```bash
pnpm dev
```

Expected: serveur démarre, pas de stack trace au boot. Visiter `http://localhost:3000` doit charger. Ctrl-C pour arrêter.

- [ ] **Step 4.5: Commit**

Run:

```bash
git add lib/auth.ts
git commit -m "feat(auth): enable Better Auth organization plugin"
```

---

## Task 5: Smoke test — vérifier que le plugin est branché

**Files:**

- Create: `scripts/verify-org-signup.ts`
- Modify: `package.json` (ajout du script)

- [ ] **Step 5.1: Écrire le script de smoke test (test "rouge" : doit échouer car le hook n'est pas encore branché)**

Créer `scripts/verify-org-signup.ts` :

```ts
/* eslint-disable no-console */
import { eq } from "drizzle-orm";
import { nanoid } from "nanoid";

import { auth } from "../lib/auth";
import { db } from "../lib/db";
import { member, organization, user } from "../lib/db/schema";

const TEST_EMAIL = `smoke-${nanoid(8)}@elevaseo.com`;
const TEST_NAME = "Smoke Test";
const TEST_PASSWORD = "Smoke1234!";

async function main() {
  console.log(`→ Signing up ${TEST_EMAIL}`);
  const res = await auth.api.signUpEmail({
    body: { name: TEST_NAME, email: TEST_EMAIL, password: TEST_PASSWORD },
  });
  const userId = res.user.id;
  console.log(`→ User created: ${userId}`);

  // Vérifier qu'une org existe pour ce user
  const memberships = await db.select().from(member).where(eq(member.userId, userId));
  if (memberships.length !== 1) {
    throw new Error(`Expected 1 membership for new user, got ${memberships.length}`);
  }
  const mem = memberships[0]!;
  if (mem.role !== "owner") {
    throw new Error(`Expected role "owner", got "${mem.role}"`);
  }

  const orgs = await db.select().from(organization).where(eq(organization.id, mem.organizationId));
  if (orgs.length !== 1) {
    throw new Error(`Expected 1 organization, got ${orgs.length}`);
  }
  const org = orgs[0]!;
  console.log(`→ Org created: id=${org.id} name="${org.name}" slug=${org.slug}`);

  // Cleanup — delete user (cascade should remove member + org via FK on member,
  // but org itself is NOT cascade-deleted via user — so delete it explicitly)
  await db.delete(organization).where(eq(organization.id, org.id));
  await db.delete(user).where(eq(user.id, userId));
  console.log(`→ Cleanup done`);

  console.log(`✅ verify-org-signup OK`);
  process.exit(0);
}

main().catch((e) => {
  console.error(`❌ verify-org-signup FAILED:`, e instanceof Error ? e.message : e);
  process.exit(1);
});
```

- [ ] **Step 5.2: Ajouter le script à `package.json`**

Dans la section `"scripts"`, ajouter (juste après `db:seed`) :

```json
"verify:org-signup": "tsx --env-file-if-exists=.env.local --env-file-if-exists=.env scripts/verify-org-signup.ts",
```

- [ ] **Step 5.3: Lancer le script — il doit ÉCHOUER**

Run:

```bash
pnpm verify:org-signup
```

Expected: ❌ FAIL avec `Expected 1 membership for new user, got 0`.

Justification : à ce stade le plugin est branché (les tables existent) MAIS le hook qui crée la Personal Org au signup n'existe pas encore. C'est le step "test rouge" du TDD.

- [ ] **Step 5.4: Commit le script (état rouge)**

Run:

```bash
git add scripts/verify-org-signup.ts package.json
git commit -m "test: add smoke test for org auto-creation at signup (red)"
```

---

## Task 6: Implémenter `createPersonalOrg(user)` dans `lib/onboarding.ts`

**Files:**

- Create: `lib/onboarding.ts`

- [ ] **Step 6.1: Créer le helper d'onboarding**

Créer `lib/onboarding.ts` :

```ts
import { eq } from "drizzle-orm";
import { nanoid } from "nanoid";

import { db } from "@/lib/db";
import { member, organization } from "@/lib/db/schema";

type NewUserInput = {
  id: string;
  email: string;
  name: string;
};

/* Idempotent: returns existing org if the user already has one (owner).
   Creates a Personal Org + an owner membership otherwise. Used by:
   - lib/auth.ts databaseHooks.user.create.after  (new signups)
   - scripts/backfill-orgs.ts                     (existing users without org) */
export async function createPersonalOrg(input: NewUserInput): Promise<{ orgId: string }> {
  // Idempotence: already a member somewhere?
  const existing = await db.select().from(member).where(eq(member.userId, input.id)).limit(1);
  if (existing.length > 0) {
    return { orgId: existing[0]!.organizationId };
  }

  const orgId = nanoid();
  const slug = `personal-${nanoid(10).toLowerCase()}`;
  const orgName = `Personal — ${input.email}`;

  await db.transaction(async (tx) => {
    await tx.insert(organization).values({
      id: orgId,
      name: orgName,
      slug,
      logo: null,
      metadata: null,
    });
    await tx.insert(member).values({
      id: nanoid(),
      organizationId: orgId,
      userId: input.id,
      role: "owner",
    });
  });

  return { orgId };
}
```

- [ ] **Step 6.2: Vérifier que ça type-check**

Run:

```bash
pnpm typecheck
```

Expected: PASS.

---

## Task 7: Brancher le hook signup → `createPersonalOrg`

**Files:**

- Modify: `lib/auth.ts`

- [ ] **Step 7.1: Importer `createPersonalOrg`**

Dans `lib/auth.ts`, ajouter avec les autres imports `@/lib/...` :

```ts
import { createPersonalOrg } from "@/lib/onboarding";
```

- [ ] **Step 7.2: Étendre le hook `after` pour appeler `createPersonalOrg`**

Modifier le hook existant. **Garder** l'insertion `leads` existante :

```ts
databaseHooks: {
  user: {
    create: {
      before: async (data) => {
        const validation = await validateProEmail(data.email);
        if (!validation.valid) {
          throw new APIError("BAD_REQUEST", {
            message: proEmailRejectMessage(validation.reason),
          });
        }
        return { data };
      },
      after: async (data) => {
        // Personal Org — every signup gets one (Phase 1).
        await createPersonalOrg({ id: data.id, email: data.email, name: data.name });

        // Marketing lead capture (existing behavior).
        await db.insert(schema.leads).values({
          id: nanoid(),
          email: data.email,
          source: "signup",
          consent: false,
          userId: data.id,
        });
      },
    },
  },
},
```

**Note d'ordre** : on crée la Personal Org **avant** le lead pour que si jamais le lead insert échoue, le user a quand même sa membership. L'inverse ferait un user signup avec lead mais sans org — état cassé.

- [ ] **Step 7.3: Lancer le smoke test — il doit maintenant PASSER**

Run:

```bash
pnpm verify:org-signup
```

Expected: ✅ `verify-org-signup OK`.

Vérifier en DB qu'il ne reste rien (cleanup OK) :

```bash
docker compose exec postgres psql -U postgres -d eleva_speed -c 'SELECT COUNT(*) FROM "user" WHERE email LIKE '\''smoke-%'\'';'
```

Expected: `0`.

- [ ] **Step 7.4: Commit (état vert)**

Run:

```bash
git add lib/onboarding.ts lib/auth.ts
git commit -m "feat(auth): auto-create Personal Org at signup via createPersonalOrg hook"
```

---

## Task 8: Script de backfill pour les users existants

**Files:**

- Create: `scripts/backfill-orgs.ts`
- Modify: `package.json`

- [ ] **Step 8.1: Constater l'état actuel des users sans org**

Run:

```bash
docker compose exec postgres psql -U postgres -d eleva_speed -c \
  "SELECT u.id, u.email FROM \"user\" u LEFT JOIN member m ON m.user_id = u.id WHERE m.id IS NULL;"
```

Expected: au moins 1 row (le user `bastien@elevaseo.com` créé avant Phase 1).

- [ ] **Step 8.2: Créer le script de backfill**

Créer `scripts/backfill-orgs.ts` :

```ts
/* eslint-disable no-console */
import { db } from "../lib/db";
import { member, user } from "../lib/db/schema";
import { createPersonalOrg } from "../lib/onboarding";

async function main() {
  // Strategy: deux selects + diff en mémoire. Drizzle ne supporte pas
  // trivialement "leftJoin … where right is null" sans plonger dans du SQL
  // raw, et le volume de users en dev/early-stage est faible (< 1k).
  // À ré-évaluer si la table user grossit massivement.
  const allUsers = await db.select({ id: user.id, email: user.email, name: user.name }).from(user);
  const allMembers = await db.select({ userId: member.userId }).from(member);
  const userIdsWithMembership = new Set(allMembers.map((m) => m.userId));
  const orphans = allUsers.filter((u) => !userIdsWithMembership.has(u.id));

  console.log(`→ Found ${orphans.length} user(s) without org`);

  for (const u of orphans) {
    const { orgId } = await createPersonalOrg({ id: u.id, email: u.email, name: u.name });
    console.log(`  ✓ ${u.email} → org ${orgId}`);
  }

  console.log(`✅ backfill-orgs done (${orphans.length} processed)`);
  process.exit(0);
}

main().catch((e) => {
  console.error(`❌ backfill-orgs FAILED:`, e instanceof Error ? e.message : e);
  process.exit(1);
});
```

**Note de design** : `createPersonalOrg` est idempotent (early-return si membership existe déjà), donc même si le script tourne deux fois ou si un user apparaissait à tort dans `orphans`, on est safe.

- [ ] **Step 8.3: Ajouter le script à `package.json`**

Dans la section `"scripts"`, ajouter (juste après `db:seed`) :

```json
"db:backfill-orgs": "tsx --env-file-if-exists=.env.local --env-file-if-exists=.env scripts/backfill-orgs.ts",
```

- [ ] **Step 8.4: Lancer le backfill**

Run:

```bash
pnpm db:backfill-orgs
```

Expected: log "Found N user(s) without org" suivi d'une ligne par user, puis "✅ backfill-orgs done".

- [ ] **Step 8.5: Vérifier l'état DB post-backfill**

Run:

```bash
docker compose exec postgres psql -U postgres -d eleva_speed -c \
  "SELECT u.email, o.name, m.role FROM \"user\" u JOIN member m ON m.user_id = u.id JOIN organization o ON o.id = m.organization_id ORDER BY u.email;"
```

Expected: chaque user a une row avec son email, le nom de son org (`Personal — <email>`), et role `owner`.

Re-run :

```bash
docker compose exec postgres psql -U postgres -d eleva_speed -c \
  "SELECT u.id FROM \"user\" u LEFT JOIN member m ON m.user_id = u.id WHERE m.id IS NULL;"
```

Expected: `0 rows`.

- [ ] **Step 8.6: Vérifier l'idempotence — re-runner doit être no-op**

Run:

```bash
pnpm db:backfill-orgs
```

Expected: `Found 0 user(s) without org` puis `✅ backfill-orgs done (0 processed)`.

- [ ] **Step 8.7: Commit le script de backfill**

Run:

```bash
git add scripts/backfill-orgs.ts package.json
git commit -m "feat(db): add idempotent backfill script for Personal Org on existing users"
```

---

## Task 9: Validation finale Phase 1

**Files:** (aucun nouveau changement, juste de la vérification)

- [ ] **Step 9.1: Re-runner le smoke test signup**

Run:

```bash
pnpm verify:org-signup
```

Expected: ✅ PASS.

- [ ] **Step 9.2: Format / lint / typecheck**

Run:

```bash
pnpm format:check && pnpm lint && pnpm typecheck
```

Expected: tous PASS.

Si format check échoue : `pnpm format` puis re-commit.
Si lint échoue : fixer les erreurs (souvent imports inutilisés ou ordering).

- [ ] **Step 9.3: Vérifier que `deadcode` ne flag pas le nouveau code**

Run:

```bash
pnpm deadcode
```

Expected: `createPersonalOrg` est utilisée (dans `lib/auth.ts` et `scripts/backfill-orgs.ts`), les nouvelles tables (`organization`, `member`, `invitation`) sont exportées depuis `schema.ts` et utilisées. Pas de nouveau dead code.

Si quelque chose est flag :

- `member` exporté mais "unused" → c'est faux positif (utilisé via Drizzle schema import). Ajouter à `knip.json` `ignoreExportsUsedInFile` si besoin, sinon laisser.
- `invitation` flag comme "unused" → **attendu** : on n'utilise pas la table avant la Phase 4 (invitations email). On ajoute une exception dans `knip.json` pour cette phase OU on accepte le warning (à documenter dans `docs/planning/pre-prod-checklist.md` comme "à utiliser en Phase 4").

- [ ] **Step 9.4: Vérification manuelle E2E via le signup réel**

Run dans 2 terminaux :

```bash
# T1
pnpm dev
```

```bash
# T2 (après que dev a démarré)
docker compose exec postgres psql -U postgres -d eleva_speed -c 'SELECT COUNT(*) FROM organization;'
```

Note le nombre N.

Dans un navigateur, visiter `http://localhost:3000/signup` et créer un compte avec un email pro réel (ex `prenom.nom@<une-vraie-entreprise>.com` — la validation `validateProEmail` doit passer).

Après signup, re-run :

```bash
docker compose exec postgres psql -U postgres -d eleva_speed -c 'SELECT COUNT(*) FROM organization;'
```

Expected: N+1.

Re-run :

```bash
docker compose exec postgres psql -U postgres -d eleva_speed -c \
  "SELECT o.name, m.role FROM organization o JOIN member m ON m.organization_id = o.id ORDER BY o.created_at DESC LIMIT 1;"
```

Expected: la nouvelle org avec le nouvel email, role `owner`.

Ctrl-C dans T1 pour stopper dev.

- [ ] **Step 9.5: Mettre à jour `docs/planning/pre-prod-checklist.md` si nécessaire**

Si le `knip` ajoute des warnings sur `invitation` (table non encore utilisée Phase 1), ajouter une entrée :

```markdown
- [ ] **`invitation` table créée mais non utilisée** : Phase 1 introduit la table mais elle n'est utilisée qu'à partir de Phase 4 (invitations équipe). Si knip warn, ignorer jusqu'à Phase 4.
```

Sinon skip ce step.

- [ ] **Step 9.6: Commit final**

Si du contenu a été modifié dans pre-prod-checklist.md ou knip.json :

```bash
git add docs/planning/pre-prod-checklist.md knip.json 2>/dev/null
git commit -m "chore: track Phase 1 unused table (invitation) until Phase 4 wires it"
```

Sinon : skip.

- [ ] **Step 9.7: Voir le log commits Phase 1**

Run:

```bash
git log --oneline -10
```

Expected: voir 4-5 commits de Phase 1 :

1. `feat(db): add Better Auth organization tables`
2. `feat(auth): enable Better Auth organization plugin`
3. `test: add smoke test for org auto-creation at signup (red)`
4. `feat(auth): auto-create Personal Org at signup via createPersonalOrg hook`
5. `feat(db): add idempotent backfill script for Personal Org on existing users`
6. (optionnel) `chore: track Phase 1 unused table`

---

## Critères d'acceptation Phase 1

- ✅ Tables `organization`, `member`, `invitation` créées en DB.
- ✅ Colonne `session.active_organization_id` ajoutée.
- ✅ Tout nouveau signup crée automatiquement une Personal Org + un member owner.
- ✅ Tous les users existants ont une Personal Org via `pnpm db:backfill-orgs`.
- ✅ `pnpm verify:org-signup` PASS.
- ✅ `pnpm format:check && pnpm lint && pnpm typecheck` PASS.
- ✅ Les pages existantes (`/dashboard`, `/admin`, `/`, `/signup`, `/login`) marchent toujours (les queries domaine ne sont pas encore scoped par org — c'est la Phase 2).
- ✅ `lib/onboarding.ts::createPersonalOrg` est idempotent (testé par re-run du backfill).

## Next phase

Une fois Phase 1 mergée dans `dev`, on rédige le plan Phase 2 : **migration `es_site` et `es_site_test` pour ajouter `organization_id`, refactor des queries domaine, rename `user_id` → `created_by_user_id` / `triggered_by_user_id`**.
