Skip to content

ADR: Remplacement de Sanity par Cloudflare D1 pour les Formulaires et Alertes Email Routing

ADR: Remplacement de Sanity par Cloudflare D1 pour les Formulaires et Alertes Email Routing

Décision architecturale remplaçant l'archivage Sanity des soumissions par une base de données Cloudflare D1 avec notifications immédiates via Cloudflare Email Routing.

  • Statut : Proposé (En attente de validation)
  • Date : 2026-10-29
  • Auteur : Antigravity & Architecture AMN
  • Projet : AMN (amn-services, amn-docs, amn-blog)
  • Dépôts / Composants concernés :

1. Contexte et Problématique1/3

Jusqu’à présent, les soumissions de formulaires (qu’il s’agisse de messages de contact ou d’abonnements newsletter) étaient relayées par Cloudflare Queues vers le Worker submission-processor, qui créait un document d’audit dans le Content Lake de Sanity CMS (_type: 'submission').

1. Contexte et Problématique2/3

Cette approche initiale présente plusieurs limitations majeures :

  1. Détournement du rôle de Sanity (L1) : Sanity est un CMS headless conçu pour la gestion éditoriale du contenu public (articles, pages, configurations graphiques). L’utiliser comme base de données transactionnelle de formulaires pollue les datasets, consomme inutilement les quotas d’API Sanity (mutations d’écriture), et expose des messages de contact potentiellement confidentiels dans l’interface Sanity Studio.
  2. Absence de requêtage relationnel natif : Sanity stocke des documents JSON non relationnels. La consultation, le tri, le filtrage et l’exportation des soumissions nécessitent des requêtes GROQ avec pagination complexe au lieu de requêtes SQL standards.

1. Contexte et Problématique3/3

  1. Absence de notification proactive pour les messages de contact : Lorsqu’un utilisateur soumettait une demande via le formulaire de contact, aucun email d’alerte n’était envoyé à l’équipe. Le message attendait simplement d’être consulté manuellement dans Sanity Studio.
  2. Coûts et dépendances externes : Réduire les dépendances tierces (en supprimant le client Sanity et ses tokens d’écriture dans le backend L3) renforce la sécurité et l’autonomie de l’infrastructure Cloudflare.

2. Décisions Architecturales

2.1. Stockage Transactionnel : Migration intégrale vers Cloudflare D11/4

Nous décidons de remplacer complètement le stockage des soumissions dans Sanity par Cloudflare D1, la base de données relationnelle serverless (SQLite à l’Edge) native de Cloudflare.

2.1. Stockage Transactionnel : Migration intégrale vers Cloudflare D12/4

  • Schéma SQL unifié (schema.sql) :
    CREATE TABLE IF NOT EXISTS submissions (
    id TEXT PRIMARY KEY,
    type TEXT NOT NULL,
    name TEXT,
    email TEXT NOT NULL,
    message TEXT,
    source TEXT,
    submitted_at TEXT NOT NULL
    );

2.1. Stockage Transactionnel : Migration intégrale vers Cloudflare D13/4

  • Binding D1 (DB) :
    • Déclaration du binding env.DB: D1Database dans submission-processor.
    • Suppression totale de la dépendance @sanity/client du package.json et de tous les imports Sanity.
    • Suppression définitive des variables et secrets devenus obsolètes : SANITY_PROJECT_ID, SANITY_DATASET et SANITY_API_WRITE_TOKEN.

2.1. Stockage Transactionnel : Migration intégrale vers Cloudflare D14/4

  • Écriture SQL préparée : Toute soumission traitée par le consommateur de queue ou l’endpoint /test-direct est insérée via une requête préparée sécurisée contre les injections SQL :
    await env.DB.prepare(
    `INSERT INTO submissions (id, type, name, email, message, source, submitted_at) VALUES (?, ?, ?, ?, ?, ?, ?)`
    ).bind(
    submission.id,
    submission.type ?? 'contact',
    submission.name ?? null,
    submission.email,
    submission.message ?? null,
    submission.source ?? null,
    submission.submittedAt
    ).run();

2.2. Alertes Immédiates : Cloudflare Email Routing & Service d’Envoi Natif

Pour les soumissions de type contact, une alerte par email doit être immédiatement acheminée vers la boîte de réception de l’organisation.

  • Binding Natif send_email : Nous configurons le binding d’envoi d’email Cloudflare (send_email) dans la configuration Wrangler :
    "send_email": [
    {
    "name": "EMAIL"
    }
    ]
  • Variables d’environnement dédiées (non codées en dur) :
    • NOTIFICATION_EMAIL_TO : Adresse de destination de l’entreprise (ex: contact@audyssee.org ou variable d’environnement).
    • NOTIFICATION_EMAIL_FROM : Adresse d’expédition validée sur le domaine Email Routing Cloudflare (ex: hello@amn-mail-dev.lasommedesrosees.com).
  • Composition & Envoi MIME : Dès l’insertion réussie dans D1, si submission.type === 'contact', le Worker construit un message bilingue/multiformat (texte brut + HTML épuré) et l’expédie via env.EMAIL.send(emailMessage). En cas de problème d’envoi d’email, l’erreur est loguée sans bloquer l’accusé de réception (ACK) de la Queue si l’insertion D1 a déjà réussi, ou fait l’objet d’un retry contrôlé.

2.3. Préservation de la Logique Loops (Newsletter)1/2

  • La fonction registerLoopsContact est strictement préservée.
  • Pour les soumissions de type newsletter :
    1. La soumission est enregistrée dans la base D1 (submissions).
    2. Le contact est transmis de manière asynchrone à Loops (via le Form Endpoint Double Opt-in ou l’API REST avec gestion de la liste de diffusion et de l’idempotence HTTP 409).

2.3. Préservation de la Logique Loops (Newsletter)2/2

  • Pour les soumissions de type contact :
    1. La soumission est enregistrée dans la base D1 (submissions).
    2. L’alerte email est expédiée à NOTIFICATION_EMAIL_TO via Cloudflare Email Routing.
    3. Aucun appel vers Loops n’est effectué (sauf si l’utilisateur coche expressément l’inscription à la newsletter dans une future évolution).

2.4. Alignement du Worker form-handler (Producer)

  • Le rôle de form-handler reste inchangé : validation Zod en amont (contact et newsletter) et injection dans SUBMISSIONS_QUEUE.
  • Son interface Env reste minimaliste et isolée (SUBMISSIONS_QUEUE: Queue, ENVIRONMENT: string).

3. Justification des Choix Techniques

Critère Ancienne approche (Sanity) Nouvelle approche (Cloudflare D1 + Email Routing)
Rôle architectural Détournement d’un CMS éditorial L1 Base de données SQL native L3 dédiée aux données applicatives
Latence & Réseau Requête HTTPS externe vers l’API Sanity Requête SQL locale interne sur l’Edge Cloudflare (< 10ms)
Limites & Quotas Consomme les quotas d’API mutations Sanity Inclus dans le runtime Cloudflare Workers D1 (100k lectures / 50k écritures/jour en Free)
Sécurité des données Données de contact visibles dans le CMS Données isolées en base relationnelle privée, accessibles uniquement aux services L3
Notification de contact Aucune (polling manuel requis dans Sanity) Envoi temps réel sur la messagerie de l’organisation via Email Routing
Maintenance des secrets 3 secrets Sanity à maintenir (PROJECT_ID, DATASET, TOKEN) 0 secret Sanity nécessaire dans la couche L3

4. Conséquences de la Décision

4.1. Impacts Positifs1/2

  • Allègement des Workers : Suppression de @sanity/client et de ses dépendances volumineuses, réduisant la taille du bundle de submission-processor de plus de 60%.
  • Autonomie de l’écosystème Cloudflare : Pipeline 100% exécuté au sein de Cloudflare (Worker Producer -> Queue -> Worker Consumer -> D1 + Email Routing).

4.1. Impacts Positifs2/2

  • Réactivité opérationnelle : Tout message envoyé depuis la page Contact déclenche une notification par email instantanée pour l’équipe.
  • Historisation fiable : Toutes les soumissions (contact et newsletter) disposent d’un audit SQL pérenne, facilement requêtable par date, email ou source.

4.2. Points d’Attention & Actions

  • Initialisation de la base D1 : Il est nécessaire d’exécuter schema.sql sur la base locale (--local) et sur la base distante Cloudflare (--remote).
  • Validation du domaine expéditeur : L’adresse NOTIFICATION_EMAIL_FROM doit appartenir à un domaine activé sur Cloudflare Email Routing / Email Sending.
  • Migration des anciennes données : Les anciennes soumissions archivées dans Sanity restent consultables dans Sanity Studio en lecture seule sans impacter le nouveau système.
Switch to normal mode

Contents