# Requirements — intégration SEO Insight (crawl on-page + corpus sémantique)

- **Date** : 2026-07-22
- **Consommateur** : `services/analytics` de seo-insight (worker Bun), déployé **sur le même serveur** que cette stack Firecrawl.
- **Référence côté app** : `docs/superpowers/specs/2026-07-22-firecrawl-onpage-crawl-design.md` (repo seo-insight).

## Ce que l'app appelle

| Endpoint | Usage | Fréquence |
|---|---|---|
| `POST /v2/batch-scrape` | job `crawl-onpage` : ~10 URLs (top SERP, domaines distincts) en un seul batch | on-demand utilisateur |
| `GET /v2/batch-scrape/{id}` | poll du job, intervalle 3 s, deadline app 5 min | pendant un crawl |
| `POST /v2/scrape` | fetch unitaire du corpus sémantique (job `serp-semantics`), concurrence ≤ 4 par job | on-demand utilisateur |

Options systématiques : `formats: ["rawHtml"]`, `onlyMainContent: false`, `maxAge: 0` (toujours frais — décision produit), `timeout: 30000` par page. Auth : header `Authorization: Bearer ${FIRECRAWL_API_KEY}` envoyé sur chaque requête.

L'app n'utilise PAS : `/search`, `/extract`, features IA, screenshots → pas besoin d'`OPENAI_API_KEY` ni de SearXNG pour cette intégration.

## Dimensionnement (multi-tenant)

- La charge multi-clients est **absorbée par la queue BullMQ de l'instance** — c'est le contrat : l'app ne fait jamais de rafale de scrapes synchrones, elle soumet des jobs et poll. `NUM_WORKERS_PER_QUEUE` (défaut 8 dans le compose) est LE bouton de capacité ; sur 12 CPU/32G avec playwright, 8 est un bon départ — surveiller via l'UI `/admin/{BULL_AUTH_KEY}/queues` et ajuster.
- Ordre de grandeur : 1 crawl utilisateur = 1 batch de ~10 pages rendues ≈ 20-60 s de travail queue. 50 clients simultanés = ~500 pages en file — acceptable, elles s'écoulent ; la deadline app (5 min/job) peut faire échouer les jobs en queue trop longtemps si l'instance est sous-dimensionnée → augmenter `NUM_WORKERS_PER_QUEUE` avant de toucher à la deadline.

## ⚠️ Même serveur + Nginx hors Docker — réseau

Le serveur héberge un **Nginx installé hors Docker**. Conséquences :

1. **Conflit de ports 80/443** : Traefik (labels du compose) ne pourra pas binder 80/443 si Nginx les tient. Deux options — en choisir UNE et s'y tenir :
   - **Option A (recommandée, app co-localisée)** : décommenter le mapping loopback du compose (`ports: "127.0.0.1:3002:3002"`) et faire pointer l'app sur `FIRECRAWL_API_URL=http://127.0.0.1:3002` — zéro TLS, zéro proxy, latence minimale. Le port n'est PAS exposé publiquement (loopback only — vérifier avec `ss -tlnp | grep 3002`). La vérification de clé API par Traefik est alors bypassée pour le trafic local : acceptable (même machine), l'app envoie quand même le header. Si aucun accès externe n'est nécessaire, retirer les labels Traefik du compose (ou laisser Traefik éteint).
   - **Option B (accès externe requis, ex. dev depuis le laptop)** : Nginx hors Docker devient l'ingress → `server` block `crawl.elevaseo.app` en `proxy_pass http://127.0.0.1:3002` avec vérification du header (`if ($http_authorization != "Bearer <clé>") { return 403; }` ou `map`) — reproduire la garde que Traefik assurait, car le self-hosted n'a AUCUNE auth applicative. Prévoir `proxy_read_timeout 90s` (un `/v2/scrape` synchrone peut tenir ~35 s ; les endpoints batch répondent vite).
   - Les deux options peuvent coexister : app locale en 127.0.0.1:3002 (A) + Nginx pour l'externe (B).
2. **Ne jamais** mapper `0.0.0.0:3002` : l'API sans auth applicative serait ouverte au monde.

## Variables d'env attendues côté app (service analytics)

```
FIRECRAWL_API_URL=http://127.0.0.1:3002    # option A (ou https://crawl.elevaseo.app en option B)
FIRECRAWL_API_KEY=<le même que .env de la stack>
```

Absentes → les jobs `crawl-onpage` échouent proprement et le corpus sémantique retombe sur le fetch legacy (dev sans instance). Aucun fallback fetch direct quand elles sont présentes.

## Plus tard : proxy anti-blacklist

Quand le taux de pages `blocked` (visible dans les totals des jobs et les chips de l'app) le justifiera : brancher un proxy résidentiel rotatif via `PROXY_SERVER` (+ `PROXY_USERNAME`/`PROXY_PASSWORD`) dans le `.env` de la stack — zéro changement côté app. Suivi : `docs/project/imports-follow-ups.md` (repo seo-insight).
