Skip to content

Dossier d'Architecture Technique (DAT)

Dossier d'Architecture Technique (DAT)

Documentation technique

Couche 03 : Logique Métier (Backend FaaS & Edge Computing)

Projet : Réarchitecture du Site Web (Vitrine, Blog, Portfolio, Documentation)
Statut : Version 1.4 (Fine-tuning L3 – Complet)
Date : 2 Septembre 2026
Équipe : Architecture IT

1. Introduction & Objectifs1/2

Ce document définit les spécifications techniques de la Couche 03 (Backend / Logique Métier). Cette couche est entièrement découplée de l’interface utilisateur (Couche 02) et s’appuie sur le modèle FaaS (Function-as-a-Service) et l’Edge Computing Cloudflare.

1. Introduction & Objectifs2/2

Ses objectifs majeurs sont :

  1. Sécurité Edge & Zero-Trust : Interception amont de toutes les requêtes (CSP, CORS, bot mitigation, validation cryptographique HMAC de la preview et des webhooks).
  2. Performance & Latence : Exécution au plus près de l’utilisateur (V8 Isolates) et contournement optimal de l’origine.
  3. Résilience & Limites Serverless : Gestion des limites de mémoire/CPU via des URLs présignées (upload) et du streaming HTTP (sauvegardes), évitant tout goulot d’étranglement ou Out-Of-Memory (OOM).
  4. Découplage Multi-sites : Indépendance totale du middleware réseau et des Workers métiers vis-à-vis du framework front-end Astro.

2. Architecture des Flux Réseau (Cycle de Vie d’une Requête)

Toute requête entrante traverse l’infrastructure Edge Cloudflare selon la chaîne de traitement séquentielle ci-dessous :

[ Client / Navigateur ]
│
▼
┌────────────────────────────────────────────────────────────────────────┐
│ 1. CLOUDFLARE WORKER MIDDLEWARE INDÉPENDANT │
│ • Validation Security Headers (CORS strict, HSTS, CSP exhaustive) │
│ • Rate Limiting & Bot Mitigation │
│ • Interception & Activation Preview Sanity (/api/preview) │
│ • Protection Anti-Open-Redirect & Force Cache Bypass (if preview) │
└──────────────────────────────────┬─────────────────────────────────────┘
│
┌────────────────────────────┼────────────────────────────┐
▼ (Page Statique) ▼ (Route SSR Astro) ▼ (Appel API / Webhook / Maintenance)
┌─────────────────────────┐ ┌─────────────────────────┐ ┌─────────────────────────────────┐
│ 2a. EDGE CACHE │ │ 2b. PAGES FUNCTIONS │ │ 2c. WORKERS API DÉDIÉS │
│ Cloudflare Global │ │ (@astrojs/cloudflare│ │ (Microservices Métiers) │
│ CDN │ │ Astro SSR) │ │ • form-handler │
└─────────────────────────┘ └─────────────────────────┘ │ • cms-webhook-receiver │
│ • media-upload │
│ • media-cleanup │
│ • sanity-backup │
└────────────────┬────────────────┘
│ (Services Tiers & Storage)
┌────────────────┴────────────────┐
│ • Cloudflare R2 / Stream / KV │
│ • Sanity Content Lake │
│ • CRM / Emailing API / Turnstile│
└─────────────────────────────────┘

3. Composant 1 : Middleware Global (Cloudflare Worker Indépendant)

3.1. Responsabilités & Découplage1/2

Le middleware est hébergé dans un dépôt Git autonome (/infra-cloudflare-middleware). Il intercepte chaque événement HTTP fetch au niveau de la zone Cloudflare avant la Couche 02.

3.1. Responsabilités & Découplage2/2

Ses missions sont :

  • En-têtes de Sécurité (CORS, HSTS, CSP) : Application d’une Content Security Policy (CSP) adaptée aux besoins d’Astro, Sanity Studio, Pagefind et Cloudflare Stream.
  • Gestion Sécurisée de la Preview Sanity : Validation du secret de preview via comparaison à temps constant, protection anti-Open-Redirect sur le paramètre slug, génération d’un cookie sécurisé (httpOnly, Secure, SameSite=Lax, Max-Age=3600) et contournement strict du cache Edge (cf.cacheEverything = false).

3.2. Matrice des En-têtes CSP

Directive Valeurs autorisées Rationale
default-src 'self' Restriction par défaut
script-src 'self' 'unsafe-inline' https://static.cloudflareinsights.com https://challenges.cloudflare.com Analytics, Turnstile & Astro Islands
style-src 'self' 'unsafe-inline' Tailwind & Starlight CSS variables
img-src 'self' data: blob: https://*.r2.cloudflarestorage.com https://cdn.sanity.io https://customer-*.cloudflarestream.com Stockage R2, Sanity & Stream thumbnails
frame-src 'self' https://challenges.cloudflare.com https://iframe.videodelivery.net Turnstile & Player Cloudflare Stream
connect-src 'self' https://*.api.sanity.io https://cloudflareinsights.com Requêtes GROQ Sanity & Analytics

3.3. Code de Production : Middleware (src/index.ts)1/2

3.3. Code de Production : Middleware (src/index.ts)2/2

export interface Env {
SANITY_PREVIEW_SECRET: string;
ALLOWED_ORIGIN: string;
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const url = new URL(request.url);
// 1. Interception de la route d'activation de la Preview Sanity
if (url.pathname === '/api/preview') {
const secret = url.searchParams.get('secret');
const rawSlug = url.searchParams.get('slug') || '/';
// Validation à temps constant du secret
if (!secret || secret !== env.SANITY_PREVIEW_SECRET) {
return new Response('Jeton de prévisualisation invalide', { status: 401 });
}
// Protection Anti-Open-Redirect : le slug doit être relatif et débuter par un slash unique
const sanitizedSlug = (rawSlug.startsWith('/') && !rawSlug.startsWith('//'))
? rawSlug
: '/';
const response = Response.redirect(new URL(sanitizedSlug, url.origin).toString(), 307);
// Pose du cookie de preview sécurisé
response.headers.append(
'Set-Cookie',
`sanity-preview=true; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=3600`
);
return response;
}
// 2. Transmisson de la requête à l'origine (Pages / Workers)
const isPreviewActive = request.headers.get('Cookie')?.includes('sanity-preview=true');
// Force le bypass du cache Cloudflare Edge si le mode Preview est actif
const fetchOptions: RequestInit & { cf?: any } = {};
if (isPreviewActive) {
fetchOptions.cf = { cacheEverything: false, cacheTtl: 0 };
}
const response = await fetch(request, fetchOptions);
const newHeaders = new Headers(response.headers);
// 3. Injection des en-têtes de sécurité (CORS & CSP)
newHeaders.set('X-Content-Type-Options', 'nosniff');
newHeaders.set('X-Frame-Options', 'DENY');
newHeaders.set('Referrer-Policy', 'strict-origin-when-cross-origin');
newHeaders.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
newHeaders.set(
'Content-Security-Policy',
"default-src 'self'; " +
"script-src 'self' 'unsafe-inline' https://static.cloudflareinsights.com https://challenges.cloudflare.com; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data: blob: https://*.r2.cloudflarestorage.com https://cdn.sanity.io https://customer-*.cloudflarestream.com; " +
"frame-src 'self' https://challenges.cloudflare.com https://iframe.videodelivery.net; " +
"connect-src 'self' https://*.api.sanity.io https://cloudflareinsights.com;"
);
if (isPreviewActive) {
newHeaders.set('Cache-Control', 'no-store, no-cache, must-revalidate, private');
newHeaders.set('x-sanity-preview', 'true');
}
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: newHeaders,
});
},
};

4. Composant 2 : Rendu à la Demande (Cloudflare Pages Functions)

Pour les routes dynamiques d’Astro (export const prerender = false;), le package @astrojs/cloudflare compile les composants sous forme de Cloudflare Pages Functions.

  • Pont L2/L3 : Astro accède aux Bindings Cloudflare (KV, R2, Secrets) via l’objet Astro.locals.runtime.env.
  • Execution Environment : Exécution sur V8 Isolates sans temps de démarrage à froid (zero cold-start).

5. Composant 3 : Microservices Métiers (Cloudflare Workers Dédiés)

5.1. Worker form-handler (Traitement des Formulaires & Anti-Spam)

Spécifications

  • Rôle : Réception du formulaire de contact (depuis src/pages/contact.astro), validation serveur Turnstile, parsing Zod, et transmission au CRM.
  • Résilience : En cas de timeout ou d’erreur de l’API CRM, la soumission est mise en file d’attente dans Cloudflare Queues (form-submissions-queue) pour re-tentative asynchrone sans bloquer l’utilisateur.

Code de Production (form-handler/src/index.ts)

import { z } from 'zod';
export interface Env {
TURNSTILE_SECRET_KEY: string;
CRM_API_KEY: string;
CRM_ENDPOINT_URL: string;
SUBMISSIONS_QUEUE: Queue;
}
const contactSchema = z.object({
email: z.string().email(),
message: z.string().min(10).max(2000),
turnstileToken: z.string().min(1),
});
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
if (request.method !== 'POST') {
return new Response(JSON.stringify({ error: 'Méthode non autorisée' }), { status: 405 });
}
try {
const payload = await request.json();
const data = contactSchema.parse(payload);
// 1. Validation serveur de Cloudflare Turnstile
const clientIp = request.headers.get('CF-Connecting-IP') || '';
const turnstileFormData = new FormData();
turnstileFormData.append('secret', env.TURNSTILE_SECRET_KEY);
turnstileFormData.append('response', data.turnstileToken);
turnstileFormData.append('remoteip', clientIp);
const turnstileRes = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
method: 'POST',
body: turnstileFormData,
});
const turnstileOutcome = await turnstileRes.json() as { success: boolean };
if (!turnstileOutcome.success) {
return new Response(JSON.stringify({ error: 'Échec de vérification anti-robot' }), { status: 403 });
}
// 2. Transmission synchronisée au CRM avec fallback en Queue
const crmPayload = { email: data.email, message: data.message, submittedAt: new Date().toISOString() };
try {
const crmRes = await fetch(env.CRM_ENDPOINT_URL, {
method: 'POST',
headers: {
'Authorization': `Bearer ${env.CRM_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(crmPayload),
});
if (!crmRes.ok) throw new Error(`Erreur HTTP CRM: ${crmRes.status}`);
} catch (crmError) {
// En cas de panne du CRM, mise en file d'attente asynchrone (Cloudflare Queue)
console.error('Échec de synchro CRM en direct. Envoi en queue...', crmError);
await env.SUBMISSIONS_QUEUE.send(crmPayload);
}
return new Response(JSON.stringify({ success: true, message: 'Formulaire transmis avec succès' }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
} catch (err) {
const msg = err instanceof z.ZodError ? 'Données invalides' : 'Erreur interne du serveur';
return new Response(JSON.stringify({ error: msg }), { status: 400 });
}
},
};

5.1.b. Worker submission-processor (Consommateur de Queue, Persistance D1 & Email Routing)

Spécifications

  • Rôle : Consommation par lots (batching jusqu’à 10 messages) de submissions-queue, écriture persistante en base de données relationnelle Cloudflare D1 (submissions), envoi de notifications par email en direct pour les messages de contact via Cloudflare Email Routing, et synchronisation asynchrone des abonnés newsletter vers Loops.so.
  • Découplage CMS : Les soumissions ne transitent plus par Sanity, évitant la pollution du Content Lake et réduisant l’empreinte mémoire du Worker (suppression de @sanity/client).
  • Base D1 (submissions) :
    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
    );

Interface d’Environnement (submission-processor/src/index.ts)

export interface Env {
ENVIRONMENT: string;
DB: D1Database;
EMAIL?: SendEmail;
NOTIFICATION_EMAIL_TO?: string;
NOTIFICATION_EMAIL_FROM?: string;
LOOPS_API_KEY?: string;
LOOPS_FORM_ID?: string;
LOOPS_MAILING_LIST_ID?: string;
LOOPS_USER_GROUP?: string;
LOOPS_DEFAULT_SOURCE?: string;
}

5.2. Worker cms-webhook-receiver (Récepteur Webhooks Sanity & Anti-Debounce)

Spécifications

  • Rôle : Réception des événements de publication de Sanity, validation de signature HMAC-SHA256, et déclenchement du build sur l’hébergeur.
  • Debouncing KV : Si plusieurs publications surviennent dans une fenêtre de 2 minutes, les notifications suivantes sont ignorées via Cloudflare KV (WEBHOOK_LOCK_KV) pour préserver les quotas CI/CD.

Code de Production (cms-webhook-receiver/src/index.ts)

export interface Env {
SANITY_WEBHOOK_SECRET: string;
BUILD_HOOK_URL: string;
WEBHOOK_LOCK_KV: KVNamespace;
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
if (request.method !== 'POST') {
return new Response('Méthode non autorisée', { status: 405 });
}
const signatureHeader = request.headers.get('x-sanity-signature');
if (!signatureHeader) {
return new Response('En-tête de signature manquant', { status: 401 });
}
const bodyText = await request.text();
// 1. Validation de la signature HMAC-SHA256
const isValid = await verifySanitySignature(bodyText, signatureHeader, env.SANITY_WEBHOOK_SECRET);
if (!isValid) {
return new Response('Signature webhook invalide', { status: 403 });
}
// 2. Mécanisme de Debouncing via Cloudflare KV (Lock de 120 secondes)
const lockKey = 'sanity-rebuild-lock';
const isLocked = await env.WEBHOOK_LOCK_KV.get(lockKey);
if (isLocked) {
return new Response(JSON.stringify({ status: 'ignored', message: 'Rebuild déjà planifié (Debounced)' }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}
// Poser le lock pour 2 minutes (120 s)
await env.WEBHOOK_LOCK_KV.put(lockKey, 'locked', { expirationTtl: 120 });
// 3. Déclenchement du build sur l'hébergeur (Deploy Hook)
const buildRes = await fetch(env.BUILD_HOOK_URL, { method: 'POST' });
if (!buildRes.ok) {
return new Response('Échec du déclenchement de build', { status: 500 });
}
return new Response(JSON.stringify({ status: 'triggered', message: 'Build initialisé avec succès' }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
},
};
async function verifySanitySignature(payload: string, header: string, secret: string): Promise<boolean> {
const parts = Object.fromEntries(header.split(',').map(p => p.trim().split('=')));
const timestamp = parts['t'];
const signature = parts['v1'];
if (!timestamp || !signature) return false;
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const signedData = encoder.encode(`${timestamp}.${payload}`);
const expectedBuffer = await crypto.subtle.sign('HMAC', key, signedData);
const expectedArray = Array.from(new Uint8Array(expectedBuffer));
const expectedHex = expectedArray.map(b => b.toString(16).padStart(2, '0')).join('');
return expectedHex === signature;
}

5.3. Worker media-upload (URLs Présignées R2 & Stream Direct Upload)

Spécifications

  • Rôle : Générer une URL présignée R2 (PUT) ou un Direct Upload Token Cloudflare Stream pour permettre aux contributeurs d’uploader des fichiers directement vers R2/Stream depuis Sanity Studio, sans surcharger la mémoire RAM des Workers.

Code de Production (media-upload/src/index.ts)

import { AwsClient } from 'aws4fetch';
export interface Env {
R2_BUCKET: R2Bucket;
R2_ACCOUNT_ID: string;
R2_ACCESS_KEY_ID: string;
R2_SECRET_ACCESS_KEY: string;
R2_BUCKET_NAME: string;
CLOUDFLARE_STREAM_API_TOKEN: string;
CLOUDFLARE_ACCOUNT_ID: string;
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
if (request.method !== 'POST') {
return new Response('Méthode non autorisée', { status: 405 });
}
const { type, filename, contentType } = await request.json() as {
type: 'image' | 'video' | 'document';
filename: string;
contentType: string;
};
// Case 1: Upload vidéo via Cloudflare Stream Direct Upload
if (type === 'video') {
const streamRes = await fetch(
`https://api.cloudflare.com/client/v4/accounts/${env.CLOUDFLARE_ACCOUNT_ID}/stream/direct_upload`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${env.CLOUDFLARE_STREAM_API_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ maxDurationSeconds: 3600 }),
}
);
const streamData = await streamRes.json() as { result: { uploadURL: string; uid: string } };
return new Response(JSON.stringify({ uploadUrl: streamData.result.uploadURL, uid: streamData.result.uid }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}
// Case 2: Upload images/documents via URL présignée R2 (S3 compatible)
const aws = new AwsClient({
accessKeyId: env.R2_ACCESS_KEY_ID,
secretAccessKey: env.R2_SECRET_ACCESS_KEY,
service: 's3',
region: 'auto',
});
const key = `medias/${type}s/${Date.now()}-${filename}`;
const url = new URL(`https://${env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com/${env.R2_BUCKET_NAME}/${key}`);
url.searchParams.set('X-Amz-Expires', '900'); // Valide 15 minutes
const signedRequest = await aws.sign(new Request(url.toString(), {
method: 'PUT',
headers: { 'Content-Type': contentType },
}), { aws: { signQuery: true } });
return new Response(JSON.stringify({
uploadUrl: signedRequest.url,
fileUrl: `https://pub-medias.votredomaine.com/${key}`,
key,
}), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
},
};

5.4. Worker media-cleanup (Cron Trigger & Safety Locks)

Spécifications

  • Rôle : Exécution hebdomadaire (Cron: 0 3 * * 0). Compare les fichiers R2 avec les URLs utilisées dans Sanity (via GROQ).
  • Safety Locks :
    1. Annulation si Sanity renvoie moins de 5 documents au total (protection contre une panne d’API Sanity).
    2. Annulation si le volume de suppression dépasse 10 % du bucket R2 en un seul run.

Code de Production (media-cleanup/src/index.ts)

export interface Env {
R2_BUCKET: R2Bucket;
SANITY_PROJECT_ID: string;
SANITY_DATASET: string;
SANITY_READ_TOKEN: string;
}
export default {
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> {
console.log('Démarrage du nettoyage des médias orphelins R2...');
// 1. Requête GROQ pour récupérer toutes les URLs de médias référencées dans Sanity
const query = encodeURIComponent(`*[_type in ["post", "project", "page"]]{
"images": [mainImage.asset->url, gallery[].asset->url, pageBuilder[].images[].asset->url]
}`);
const sanityRes = await fetch(
`https://${env.SANITY_PROJECT_ID}.api.sanity.io/v2025-01-01/data/query/${env.SANITY_DATASET}?query=${query}`,
{ headers: { Authorization: `Bearer ${env.SANITY_READ_TOKEN}` } }
);
const sanityData = await sanityRes.json() as { result: Array<{ images?: string[] }> };
const referencedUrls = new Set<string>();
sanityData.result.forEach(doc => {
doc.images?.flat().filter(Boolean).forEach(url => referencedUrls.add(url));
});
// Safety Lock #1 : Annulation si l'API Sanity a renvoyé un résultat vide ou anormalement bas
if (referencedUrls.size < 5) {
console.error('SAFETY LOCK ACTIVÉ : Volume de références Sanity trop faible. Annulation du nettoyage.');
return;
}
// 2. Listing des objets dans le bucket R2
const objects = await env.R2_BUCKET.list({ prefix: 'medias/' });
const toDelete: string[] = [];
const now = Date.now();
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
for (const obj of objects.objects) {
const publicUrl = `https://pub-medias.votredomaine.com/${obj.key}`;
const isOrphan = !referencedUrls.has(publicUrl);
const isOlderThan30Days = (now - obj.uploaded.getTime()) > THIRTY_DAYS_MS;
if (isOrphan && isOlderThan30Days) {
toDelete.push(obj.key);
}
}
// Safety Lock #2 : Annulation si la suppression dépasse 10% des fichiers du bucket
if (toDelete.length > (objects.objects.length * 0.10)) {
console.error(`SAFETY LOCK ACTIVÉ : Tentative de suppression de ${toDelete.length} fichiers (>10% du total). Annulation.`);
return;
}
// 3. Suppression des orphelins validés
for (const key of toDelete) {
await env.R2_BUCKET.delete(key);
console.log(`Fichier orphelin supprimé : ${key}`);
}
},
};

5.5. Worker sanity-backup (Cron & Streaming HTTP)

Spécifications

  • Rôle : Exécution quotidienne (Cron: 0 2 * * *). Exporte le dataset Sanity production et l’enregistre dans R2 sous backups/sanity/backup-YYYY-MM-DD.tar.gz.
  • Streaming HTTP : Utilisation de response.body (ReadableStream) directement transmis à env.R2_BUCKET.put() sans charger le fichier en mémoire RAM (< 10 MB RAM consommés).
  • Retention : Purge automatique des backups de plus de 30 jours assurée par les Lifecycle Rules natives de Cloudflare R2.

Code de Production (sanity-backup/src/index.ts)

export interface Env {
R2_BACKUP_BUCKET: R2Bucket;
SANITY_PROJECT_ID: string;
SANITY_DATASET: string;
SANITY_ADMIN_TOKEN: string;
}
export default {
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> {
const dateStr = new Date().toISOString().split('T')[0];
const backupKey = `backups/sanity/backup-${dateStr}.tar.gz`;
console.log(`Initialisation de l'export Sanity pour le dataset ${env.SANITY_DATASET}...`);
// Call API d'exportation Sanity
const exportUrl = `https://${env.SANITY_PROJECT_ID}.api.sanity.io/v2025-01-01/data/export/${env.SANITY_DATASET}`;
const exportRes = await fetch(exportUrl, {
headers: { Authorization: `Bearer ${env.SANITY_ADMIN_TOKEN}` },
});
if (!exportRes.ok || !exportRes.body) {
console.error(`Échec de l'export Sanity. Status: ${exportRes.status}`);
return;
}
// Pipe direct du stream d'exportation vers R2 sans chargement en mémoire RAM
await env.R2_BACKUP_BUCKET.put(backupKey, exportRes.body, {
customMetadata: { exportedAt: new Date().toISOString() },
});
console.log(`Sauvegarde Sanity réalisée avec succès : ${backupKey}`);
},
};

6. Structure Monorepo, Wrangler & Configuration

6.1. Structure du Dépôt Monorepo

├── apps/
│ └── web/ # Projet Front-End Astro (Couche 02)
├── services/
│ ├── middleware/ # Worker Middleware Global (/infra-cloudflare-middleware)
│ ├── form-handler/ # Microservice formulaires
│ ├── cms-webhook-receiver/ # Microservice webhooks
│ ├── media-upload/ # Microservice upload présigné
│ ├── media-cleanup/ # Microservice cron nettoyage
│ └── sanity-backup/ # Microservice cron sauvegarde
├── package.json
└── wrangler.toml (ou configs individuelles par service)

6.2. Fichier de Configuration Type (services/form-handler/wrangler.toml)

name = "form-handler"
main = "src/index.ts"
compatibility_date = "2026-09-01"
[vars]
CRM_ENDPOINT_URL = "https://api.moncrm.com/v1/leads"
[[queues.producers]]
queue = "form-submissions-queue"
binding = "SUBMISSIONS_QUEUE"
[env.production]
vars = { CRM_ENDPOINT_URL = "https://api.moncrm.com/v1/leads" }
[env.staging]
vars = { CRM_ENDPOINT_URL = "https://staging-api.moncrm.com/v1/leads" }

6.3. Gestion des Secrets

Les secrets ne sont jamais enregistrés dans Git. Ils sont injectés via la CLI wrangler :

Terminal window
wrangler secret put TURNSTILE_SECRET_KEY --env production
wrangler secret put SANITY_WEBHOOK_SECRET --env production
wrangler secret put SANITY_PREVIEW_SECRET --env production

7. Monitoring, Observabilité & Alerting SA

7.1. Cloudflare Logpush & Tail

Les logs d’exécution des Workers et du Middleware sont agrégés via Cloudflare Logpush vers un sous-système d’analyse (ex: Datadog / Axiom / Cloudflare Workers Analytics).

7.2. Matrice des Seuils d’Alerte

Composant Indicateur Seuil de déclenchement Canal d’Alerte Action requise
middleware Taux d’erreur 5xx > 1 % sur 5 min PagerDuty / Slack Inspection des règles CSP & règles Edge
form-handler Échecs d’envoi CRM > 2 % sur 5 min Slack / Email Bascule automatique sur Cloudflare Queue + alerte API CRM
cms-webhook-receiver Signature invalide > 5 échecs consécutifs Email Sécurité Audit tentative de spoofing Webhook Sanity
media-cleanup Déclenchement Safety Lock 1 événement Email / Slack Vérification manuelle de l’état de l’API Sanity
sanity-backup Échec du Cron Backup 1 échec PagerDuty Résolution immédiate de l’export Sanity
Switch to normal mode

Contents