# SEO Insight — modèle de données (ébauche v2)

> Statut : **draft** (2026-06-04, rév. 2). Tenancy + droits via **Better Auth**
> (plugins `organization` + `stripe`) ; tables domaine en **Drizzle/Postgres**.

## Principes

- **`organization` = l'agence/compte** (tenant). Toutes les données domaine portent `org_id` ; isolation par org.
- **`site` = `projet`** : un site EST un projet = la **marque-domaine** sur laquelle on travaille (ex. Atelier Forge — `atelierforge.com`). **Pas de table `projects` séparée.** Le switcher haut-gauche sélectionne le **site/projet actif** → scope les keywords et le travail backlink par site.
- **`link_sources` = reservoir GLOBAL** : un **annuaire/directory** de sources de liens, mutualisé au niveau **org** (base pour TOUS les sites). Ajouter une source = dispo pour tous les sites. **Non scopé par site.**
- **`backlinks` = ciblage par site** : relation source→site (`prospect | planned | live | lost`). C'est ce qui relie une source du reservoir à un site donné. Dans le reservoir, "Ajouter au site actif" crée un `backlink` prospect pour le site sélectionné.
- **Droits = `member.role`** (`owner | admin | staff`). Billing réservé owner/admin.

## Diagramme

```mermaid
erDiagram
  user ||--o{ member : ""
  organization ||--o{ member : ""
  organization ||--o{ invitation : ""
  organization ||--o| subscription : "billing"
  organization ||--o{ sites : "possède"
  organization ||--o{ link_sources : "reservoir global (directory)"
  user ||--o{ sites : "owner"
  sites ||--o{ keywords : ""
  sites ||--o{ backlinks : "ciblé par"
  link_sources ||--o{ backlinks : "placé / prospecté comme"

  organization {
    text id PK "Better Auth"
    text name
    text slug UK
  }
  member {
    text id PK
    text organization_id FK
    text user_id FK
    text role "owner | admin | staff"
  }
  subscription {
    text id PK "BA stripe plugin"
    text reference_id FK "= organization.id"
    text plan
    text status
  }
  sites {
    text id PK
    text org_id FK
    text owner_user_id FK
    text name
    text url "domaine principal"
    text primary_language "ex. fr"
    textarr languages "i18n (en, de…)"
    textarr target_countries "ex. FR, BE"
  }
  keywords {
    text id PK
    text site_id FK
    text term
    text theme
  }
  link_sources {
    text id PK
    text org_id FK "directory global"
    text domain_norm "non unique (path-aware)"
    text platform
    int da
    text kind
  }
  backlinks {
    text id PK
    text link_source_id FK
    text site_id FK
    text status "prospect | planned | live | lost"
    text anchor
  }
```

## Reservoir backlink — rôle

- Vue **globale** (tous les sources de l'org), c'est l'annuaire de base.
- Action **"Ajouter au site actif"** (bulk + ligne) → `backlink(source, site_actif, prospect)`.
- Colonne **Footprint** = combien de mes sites ciblent déjà cette source (agrégat `backlinks`).
- Le détail du triage/campagne par site vit ensuite dans `recherche`/`planning`.

## Ordre d'implémentation (rév.)

1. **Better Auth** ✅ (orgs/users/members/roles).
2. **Schéma domaine** : `sites` (+ url/langues/pays), `keywords`, `link_sources` (directory global), `backlinks`. **Supprimé** : `projects`, `link_source_projects`.
3. **Seed** : 1 org (agence) + sites (avec url/langues/pays) + 380 link_sources + quelques backlinks.
4. **UI** : reservoir global + "Ajouter au site actif", switcher = site/projet actif, form d'ajout de site (nom/url/langues/pays).

## Différé / à trancher

- Permissions fines (table policy) — non MVP.
- Stripe — quand les clés existent.
- Multi-org (plusieurs agences) : un surrogate key + `unique(org_id, source_key)` sur `link_sources` si un même domaine devait coexister entre orgs.

## Évolutions implémentées (2026-06)

Schéma réel : `src/lib/db/domain-schema.ts`. Au-delà de l'ébauche ci-dessus :

- **`keywords`** enrichi : `lang`, `country`, `status`, `intent[]`, `stages[]`, `primary_source`, `sources[]`, `serp_features[]`, et **overlays jsonb** :
  - `metrics` — `Record<source, {volume,kd,cpc,growthPct,…}>` (par fournisseur)
  - `trend` — `MonthlyVolume[]` (sparkline)
  - `briefs` — `Record<locale, KeywordBrief>` (briefs YTG/Lumenia, par pays)
  - `gsc` — `GscTracking | null` (clics/impressions/CTR/position)
  - `serp` — `Record<locale, SerpSnapshot>` (top-10 concurrents + crawl on-page : meta/images/alt par image/JSON-LD) ← slices contenu SERP 1-2
- **`keyword_themes`** (`{siteId}-{slug}`) — catégories de mots-clés par site.
- **`content_items`** — pipeline éditorial : `title`, `status` (`idea|brief|writing|review|published`), `primary_keyword_id` (→ keywords), `lang`, `country`, `assignee`, `due_date`, `url`. Le résumé d'analyse (SERP/on-page/brief) est **dérivé** à la lecture (`buildContentAnalysis`), pas stocké.
- **`sites.gsc_property`** — propriété Search Console mappée (ex. `sc-domain:example.com`).
