> ## Documentation Index
> Fetch the complete documentation index at: https://doc.fluximmo.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Répliquer la BDD adverts en sync continue

> Backfill historique sur webhook (par mail) + alerte ADVERT continue → BDD locale en miroir avec events PRICE/REPUBLISHED/UNPUBLISHED.

## Goal

Maintenir une copie locale (PostgreSQL/MySQL/ClickHouse) de toutes les annonces (`adverts`) sur un périmètre donné, **synchronisée en continu** sur votre webhook : alerte ADVERT pour le flux post-création + demande de backfill historique pour rattraper le passé.

## Scénario

Cas d'usage typiques :

* **Notification live pour un agrégateur de recherche immobilière** — pousser les nouvelles annonces vers les utilisateurs finaux dès leur ingestion ;
* **Veille temps-réel pour chasseur immobilier ou investisseur** — alertes sur publications fraîches matchant un cahier des charges ;
* **Pipeline interne de traitement / réplique de DB / exposition d'API custom dérivées** — enrichir les annonces avec ses propres champs (scoring, tags internes, attribution) et requêter sans appeler Fluximmo à chaque page vue ;
* **Capter 100% des événements de cycle de vie** — créations, changements de prix, dépublications, republications, sur l'ensemble du périmètre.

L'objectif est d'avoir, à tout instant, la BDD locale en miroir de Fluximmo sur le périmètre choisi, avec un retard \< 1 minute sur le flux courant.

## Étapes

<Steps>
  <Step title="1. Choisir Adverts plutôt que Properties">
    Pour un cas réplication, `adverts` est **toujours** le bon choix :

    * **Payload webhook complet** : l'objet advert entier est livré dans le webhook, pas uniquement un `flxId`. Pas besoin de re-fetcher.
    * **Suivi inter-portails** : un même bien apparaissant sur deux portails distincts (par exemple un grand réseau d'agences et un mandataire indépendant) = 2 adverts liés au même `propertyFlxId`.
    * **Events** : `PRICE`, `REPUBLISHED`, `UNPUBLISHED` sont émis sur l'advert (pas sur la property).
    * **Reconstitution Property côté DB** : grouper les adverts par `propertyFlxId` permet de retrouver une vue dédupliquée localement.

    Voir [Property vs Advert](/concepts/property-vs-advert) pour les détails.
  </Step>

  <Step title="2. Préparer le schéma de table locale">
    ```sql theme={null}
    CREATE TABLE adverts (
      flx_id           TEXT PRIMARY KEY,
      property_id      TEXT NOT NULL,           -- racine de la chaîne de dédup
      raw_payload      JSONB NOT NULL,
      price            INTEGER,
      last_modified_at TIMESTAMPTZ NOT NULL,
      is_online        BOOLEAN NOT NULL DEFAULT TRUE,
      unpublished_at   TIMESTAMPTZ,
      created_at       TIMESTAMPTZ NOT NULL DEFAULT NOW(),
      updated_at       TIMESTAMPTZ NOT NULL DEFAULT NOW()
    );
    CREATE INDEX idx_adverts_property ON adverts(property_id);
    CREATE INDEX idx_adverts_last_mod ON adverts(last_modified_at);

    CREATE TABLE advert_price_history (
      id          BIGSERIAL PRIMARY KEY,
      flx_id      TEXT NOT NULL REFERENCES adverts(flx_id),
      price       INTEGER NOT NULL,
      observed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
    );
    ```

    L'index `idx_adverts_property` est ce qui permettra la reconstitution de la vue Property à l'étape 6.
  </Step>

  <Step title="3. Demander l'activation de l'alerte ADVERT (création par Fluximmo)">
    La création d'alertes ADVERT sur webhook se fait **sur demande** par mail. Écrivez à [contact@fluximmo.com](mailto:contact@fluximmo.com) en précisant :

    * Votre `clientId`
    * Le `webhook_url` cible (HTTPS public, doit ack en \< 1 s)
    * Les filtres souhaités (par exemple : département 75, achat appartement, `isOnline: true`)
    * Les types de match désirés : `ALERT_MATCH_CREATED` + `ALERT_MATCH_ADVERT_EVENT` (créations + events `PRICE`/`REPUBLISHED`/`UNPUBLISHED`)
    * Le volume estimé que votre receiver peut absorber

    Une fois l'alerte créée, vous recevez son `flxId` pour la modifier ultérieurement (en `PATCH`, jamais en `DELETE` + recréation).
  </Step>

  <Step title="4. Demander le backfill historique sur le même webhook">
    L'alerte ne fait **pas de backfill automatique** : elle ne match que les adverts ingérées **après** sa création. Pour rattraper le passé, écrivez à nouveau à [contact@fluximmo.com](mailto:contact@fluximmo.com) en précisant :

    * Le `flxId` de l'alerte créée à l'étape 3
    * La période de backfill (ex. derniers 30 jours, 6 mois, 2 ans)
    * Le volume estimé que votre receiver peut absorber pendant la rejouage

    Le backfill est rejoué sur votre webhook avec le **même format de payload** que les matches normaux. Votre handler le traite sans changement de code, idempotence via `advert.flxId`.
  </Step>

  <Step title="5. Endpoint webhook : ACK rapide + queue">
    Le crawler Fluximmo attend un `200` rapide (\< 1 s). Toute logique métier doit être **différée** dans une queue (SQS / RabbitMQ / Redis Streams).

    ```text theme={null}
    # Pseudocode handler
    on POST /webhooks/fluximmo:
        if header.x-webhook-key != EXPECTED_KEY: return 401
        enqueue(request.body)            # SQS, RabbitMQ, Redis Streams
        return 200
    ```
  </Step>

  <Step title="6. Worker async : UPSERT + diff + reconstitution Property">
    Le webhook a la shape canonique `{ data: { created, updated } }` :

    * **`data.created[].adverts[]`** = `AdvertDto` complet → UPSERT total côté DB
    * **`data.updated[].adverts[]`** = DTO réduit (`flxId`, `currentPrice`, `isOnline` uniquement) → UPDATE partiel + diff vs état stocké pour dériver les events PRICE / REPUBLISHED / UNPUBLISHED

    ```text theme={null}
    # Pseudocode worker
    handle_payload(body):
        # Branche CREATED — AdvertDto complet
        for entry in body.data.created or []:
            for advert in entry.adverts:
                UPSERT INTO adverts (flx_id, property_id, raw_payload, price, last_modified_at, is_online)
                       VALUES (advert.flxId, advert.propertyFlxId, advert, advert.currentPrice.value,
                               advert.lastModifiedAt, advert.isOnline)
                INSERT INTO advert_price_history (flx_id, price, source) VALUES (advert.flxId, advert.currentPrice.value, 'CREATED')

        # Branche UPDATED — DTO réduit, events dérivés client-side
        for entry in body.data.updated or []:
            for advert in entry.adverts:
                prev = SELECT price, is_online FROM adverts WHERE flx_id = advert.flxId
                new_price  = advert.currentPrice.value
                new_online = advert.isOnline

                if prev is None:
                    # advert pas en miroir local : log + ignorer (ou alerter pour reconciliation)
                    continue

                UPDATE adverts SET price = new_price, is_online = new_online, updated_at = NOW()
                       WHERE flx_id = advert.flxId

                if new_price != prev.price:
                    INSERT INTO advert_price_history (flx_id, price, source) VALUES (advert.flxId, new_price, 'PRICE_DERIVED')
                if new_online and not prev.is_online:
                    UPDATE adverts SET unpublished_at = NULL WHERE flx_id = advert.flxId
                if not new_online and prev.is_online:
                    UPDATE adverts SET unpublished_at = NOW() WHERE flx_id = advert.flxId
    ```

    **Reconstitution de la vue Property côté DB** — quand vous voulez interroger votre miroir comme si c'était `/properties` :

    ```sql theme={null}
    -- Une "Property" = ensemble d'adverts partageant le même property_id
    SELECT
      property_id,
      count(*) AS sources_count,
      min(last_modified_at) AS first_seen_at,
      max(last_modified_at) AS last_modified_at,
      bool_or(is_online) AS has_any_online,
      -- prix médian sur les adverts encore en ligne
      percentile_cont(0.5) WITHIN GROUP (ORDER BY price) FILTER (WHERE is_online) AS median_price_online
    FROM adverts
    WHERE property_id IS NOT NULL
    GROUP BY property_id;
    ```
  </Step>

  <Step title="7. Idempotence + reconciliation">
    **Idempotence** : Fluximmo livre parfois 2 fois le même webhook (retries sur timeout). L'`UPSERT` par `flxId` rend le worker safe par construction.

    **Reconciliation** : si vous suspectez une dérive (panne queue, bug worker, perte webhook), demandez à Fluximmo un nouveau backfill ciblé sur la fenêtre concernée (par mail à `contact@fluximmo.com`, en précisant la période).
  </Step>

  <Step title="8. Préserver l'historique alerte">
    <Snippet file="/snippets/warning-cycle-vie-alerte.mdx" />

    En pratique : **PATCH** votre alerte plutôt que `DELETE` + recréation, pour conserver le lien historique et continuer à recevoir des events sur les adverts déjà matchées.
  </Step>
</Steps>

## Architecture / flow

```mermaid theme={null}
flowchart LR
  Mail["Mail à Fluximmo<br/>(création alerte<br/>+ backfill)"] --> FX[Fluximmo crawler]
  FX -- "advert ingérée<br/>ou backfill rejoué" --> Webhook[POST webhook user]
  Webhook --> Ack[ACK 200 rapide]
  Ack --> Queue[Queue SQS / Redis Streams]
  Queue --> Worker[Worker async]
  Worker -- "UPSERT (data.created)<br/>+ UPDATE partiel & diff (data.updated)" --> BDD[(BDD locale)]
  BDD --> View["Vue Property locale<br/>(GROUP BY propertyFlxId)"]
```

## Pièges fréquents

<Warning>
  **Properties au lieu de Adverts** : le webhook properties ne livre que des `flxId`, pas le payload complet, et n'émet pas d'events `PRICE`/`REPUBLISHED`/`UNPUBLISHED`. Mauvais choix pour une réplication marketplace.
</Warning>

<Warning>
  **Pas de backfill explicite** : l'alerte ne backfille pas seule. Sans étape 4 (demande par mail), vous n'aurez que les annonces **créées après** la création de l'alerte — trou historique permanent.
</Warning>

<Warning>
  **Traitement synchrone dans le webhook** : un endpoint qui fait `INSERT` puis `200` peut dépasser 1 s sous charge → timeouts → retries → effet de bord cumulatif. Toujours ACK d'abord, traiter ensuite (étape 5).
</Warning>

<Warning>
  **Pas d'idempotence** : sans `UPSERT` par `flxId`, les retries du crawler créent des doublons en BDD locale.
</Warning>

<Warning>
  **`DELETE` + recréer l'alerte** = perte des events historiques. Toujours `PATCH /alerts/{flxId}` pour modifier en place.
</Warning>

## Pour aller plus loin

* [Concepts — Property vs Advert](/concepts/property-vs-advert) (`propertyFlxId` pour reconstituer la dédup côté client)
* [Concepts — Match types & cycle d'alerte](/concepts/match-types-cycle-alerte)
* [Concepts — Webhooks](/concepts/webhooks)
* [API — PUT /adverts/search/alerts](/api-v2-reference/adverts-alerts/create-a-new-advert-alert-for-this-search)
* [API — Sample advert webhook body](/api-v2-reference/sample-adverts/sample-advert-webhook-body)
* [Playbook — Track price changes](/playbooks/track-price-changes)
* [Playbook — Webhook volume architecture](/playbooks/webhook-volume-architecture)

<Snippet file="/snippets/cta-cle-test-gratuite.mdx" />
