edtech / docs /Xamle_System_Spec_Deployment_Sheet.md
CognxSafeTrack
docs: add audit_production.md + plan_implementation_prod.md; fix admin phone field
f1a06cd

XAMLÉ – System Spec & Deployment Sheet (HF + Railway Worker)

Ce document centralise toutes les informations techniques, de configuration et d'architecture de l'infrastructure XAMLÉ (SafeTrack EdTech). Il sert de référence absolue (Single Source of Truth) pour déployer et maintenir le backend scindé entre Hugging Face (API) et Railway (Worker).


1. Résumé Architecture (Actuelle / Cible)

Le système est construit sur un monorepo Turborepo (pnpm) composé de multiples applications qui se parlent via une base de données et un cache central.

  • Hugging Face Space (Public Inbound Endpoint) :
    • Héberge l'application apps/api.
    • Rôle : Point d'entrée public pour le Webhook Meta (WhatsApp Inbound), les requêtes Stripe, et le back-office Admin.
    • C'est ici que l'IA (OpenAI) génère le contenu et que l'utilisateur est enregistré en base. Aucune requête sortante vers Meta n'est émise d'ici à cause des restrictions DNS (ENOTFOUND).
  • Railway (Outbound Background Worker) :
    • Héberge uniquement l'application apps/whatsapp-worker.
    • Rôle : Consommer les jobs BullMQ enregistrés dans Redis par Hugging Face, et envoyer physiquement les messages sortants vers l'API WhatsApp Cloud (graph.facebook.com). Contient le Cron (Scheduler) quotidien.
  • Neon Postgres :
    • Rôle : Base de données relationnelle centrale (Prisma) partagée entre HF et Railway (Tables User, Track, Enrollment, TrackDay).
  • Upstash Redis :
    • Rôle : Gestion de la file d'attente asynchrone BullMQ (queue whatsappQueue). Il agit comme le pont de communication en temps réel entre Hugging Face et Railway.
  • Cloudflare R2 :
    • Rôle : Stockage S3 public pour héberger les fichiers générés par l'IA (PDF, Audio, PPTX) afin que Meta puisse les télécharger via des URLs publiques et les envoyer aux utilisateurs WhatsApp.
  • OpenAI :
    • Rôle : LLM utilisé dans apps/api (sur Hugging Face) pour personnaliser les leçons et générer l'audio (Whisper STT / TTS).

2. URLs & Endpoints

URL de base Hugging Face : https://safetrack-edtech.hf.space

Webhook WhatsApp (Configuration Meta App Dashboard)

  • GET Verify (Public) : GET https://safetrack-edtech.hf.space/v1/whatsapp/webhook
    • Utilisé par Meta une seule fois lors de la configuration avec <WHATSAPP_VERIFY_TOKEN>.
  • POST Webhook (Public mais Sécurisé HMAC) : POST https://safetrack-edtech.hf.space/v1/whatsapp/webhook
    • L'endpoint où Meta poste les messages des utilisateurs.
    • Sécurisé via validation de signature X-Hub-Signature-256 en utilisant <WHATSAPP_APP_SECRET>.

Utilitaires & Healthchecks (Publics)

  • GET / : Healthcheck basique ({"ok": true}).
  • GET /health : Monitoring avec timestamp.
  • GET /privacy : Politique de confidentialité servie en HTML (requise par Meta App Review).
  • GET /debug/net : Teste la connectivité Google (diagnostique Egress).
  • GET /debug/graph : Teste explicitement si la résolution DNS graph.facebook.com fonctionne (Diagnostic HF DNS Block).

Routes Privées (Protégées par X-API-Key)

Les routes suivantes exigent le header Authorization: Bearer <ADMIN_API_KEY> :

  • /v1/admin/* : Endpoints pour le frontend Admin (Stats, Enrollments).
  • /v1/ai/* : Génération à la demande de PDF/Audio.
  • /v1/payments/* : Création de sessions Stripe.

3. Variables & Secrets (Environment)

Name Env Type Example / Placeholder Used by Notes
WHATSAPP_VERIFY_TOKEN HF Secret my_secure_verify_token_123 apps/api Vérifie le webhook initial de Meta.
WHATSAPP_APP_SECRET HF Secret c2a5... (32 chars) apps/api Clé secrète de l'App Meta pour signer (crypto.createHmac) les payloads.
WHATSAPP_ACCESS_TOKEN Railway & HF Secret EAA... whatsapp-worker & api Le jeton d'accès permanent System User. Worker (envoi message) / API (téléchargement audio STT).
WHATSAPP_PHONE_NUMBER_ID Railway Public 5249583... whatsapp-worker L'ID Meta du numéro expéditeur.
DATABASE_URL Both Secret postgresql://user:pass@ep-neon.tech/db?sslmode=require api & worker DSN Neon (Pooler ou direct).
REDIS_URL Both Secret rediss://default:pass@upstash.io:6379 api & worker Important: Si Upstash TLS, utiliser rediss:// (avec 2 's') et non redis://.
ADMIN_API_KEY HF Secret sk_admin_12345 apps/api Sécurise le dashboard et les routes privées.
OPENAI_API_KEY HF Secret sk-proj... apps/api Provider IA.
R2_ACCOUNT_ID HF Public a1b2c3d4... apps/api Cloudflare Storage.
R2_BUCKET HF Public xamle-edtech-assets apps/api Nom du seau Cloudflare.
R2_PUBLIC_URL Both Public https://assets.xamle.com api & worker URL de diffusion pour que l'App Meta puisse télécharger l'asset.
R2_ACCESS_KEY_ID HF Secret ... apps/api AWS S3 Compat layer credential.
R2_SECRET_ACCESS_KEY HF Secret ... apps/api AWS S3 Compat layer credential.
STRIPE_SECRET_KEY HF Secret sk_test_... apps/api Paiements et abonnements Premium.
STRIPE_WEBHOOK_SECRET HF Secret whsec_... apps/api Signature Webhook Stripe.

4. BullMQ / Worker Logic

La file d'attente garantit la résilience.

  • Nom de la Queue : whatsappQueue
  • Producer (Hugging Face) : Quand un webhook Meta arrive, l'API appelle scheduleMessage(userId, text) dans apps/api/src/services/queue.ts.
  • Consumer (Railway) : Le Worker écoute la queue dans apps/whatsapp-worker/src/index.ts. Le handler de la file est instancié avec new Worker('whatsappQueue', async (job) => { ... }).
  • Job Types (Payload JSON) :
    • Type send-message : { "userId": "uuid-1234", "text": "Bienvenue !" }
    • Type send-content : { "userId": "uuid-123", "trackDayId": "day-123" } (déclenche génération IA et envois médias).
  • Logs Attendus sur Railway : Worker: Processing job: send-message 123 [WhatsApp] ✅ Text message sent to 22177...

5. WhatsApp Cloud API

Toutes les requêtes sortantes vers Meta sont centralisées dans apps/whatsapp-worker/src/whatsapp-cloud.ts.

  • URL Exacte : https://graph.facebook.com/v18.0/<WHATSAPP_PHONE_NUMBER_ID>/messages (Forcée via Axios avec un agent HTTP configuré explicitement sur l'IPv4 family: 4 pour anticiper tout problème de configuration DNS Docker).
  • Headers :
    {
      "Content-Type": "application/json",
      "Authorization": "Bearer <WHATSAPP_ACCESS_TOKEN>"
    }
    
  • Body Type "Text" :
    {
      "messaging_product": "whatsapp",
      "recipient_type": "individual",
      "to": "221781476249",
      "type": "text",
      "text": { "preview_url": false, "body": "Hello World" }
    }
    

6. Sécurité

  1. Validations Webhook HMAC La vérification cryptographique est active dans apps/api/src/routes/whatsapp.ts.
    const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
    
  2. Rate Limiting Global (Hugging Face / Fastify) Configuré dans index.ts avec @fastify/rate-limit. Le seuil actuel est de 300 requêtes / minute par adresse IP (qui protégera l'API si le Webhook fuite mais n'impactera pas l'IP de Meta qui est distribuée).
  3. API Key (Admin routes) Vérification stricte de Bearer <ADMIN_API_KEY> sur chaque route admin/backoffice.
  4. CORS CORS est ouvert globalement via @fastify/cors pour permettre au dashboard frontal d'intéragir depuis n'importe où.

7. Repo & Build

Le projet est un monorepo géré avec pnpm et ses workspaces.

  • apps/api/ : Application Fastify (Node).
  • apps/whatsapp-worker/ : Processus BullMQ natif (Node).
  • packages/database/ : Couche Prisma partagée avec le schéma schema.prisma.

Commandes critiques (Railway Build & Start) :

  • Root command (Installation) : pnpm install
  • Génération client : pnpm --filter @repo/database build (ou prisma generate).
  • Build du Worker : pnpm --filter whatsapp-worker build
  • Start du Worker (Prod) : pnpm --filter whatsapp-worker start

Note : Le script start de whatsapp-worker utilise actuellement tsx src/index.ts en développement. Pour la prod (Railway), il est conseillé de compiler avec tsc et lancer node dist/index.js.


8. Plan de Déploiement "Railway Worker Only"

Pour déployer le Worker sur Railway, nous voulons ignorer totalement l'API et le Frontend. La configuration Railway cible le dossier du worker :

  1. Connect GitHub : Ajouter le repository Edtech à Railway.
  2. Configuration du Service (Settings) :
    • Root Directory : Laisser à / (car c'est un monorepo pnpm, Railway comprendra les workspaces).
    • Build Command : pnpm install && pnpm --filter @repo/database build && pnpm --filter whatsapp-worker build
    • Start Command : pnpm --filter whatsapp-worker start
  3. Injector les Variables (Variables tab) :
    • Coller en Block Import les variables requises signalées en "Railway" ou "Both" dans la section [3].
    • Spécialement DATABASE_URL, REDIS_URL, WHATSAPP_PHONE_NUMBER_ID, et WHATSAPP_ACCESS_TOKEN.
  4. Checklist de validation finale :
    • Envoyer INSCRIPTION au numéro WhatsApp.
    • Vérifier les logs sur le container de Hugging Face => on y voit la réception [WEBHOOK] Received webhook event....
    • Ouvrir les logs de Railway => on y voit le worker prendre la relève Processing job: send-message.
    • Le téléphone reçoit le message instantanément.

9. Extraits de code de référence

A. Producer (Hugging Face) -> services/queue.ts

import { Queue } from 'bullmq';
import Redis from 'ioredis';

const redisConnection = new Redis(process.env.REDIS_URL!);
export const whatsappQueue = new Queue('whatsappQueue', { connection: redisConnection });

export async function scheduleMessage(userId: string, text: string, delayMs: number = 0) {
    await whatsappQueue.add('send-message', { userId, text }, { delay: delayMs });
}

B. Consumer (Railway Worker) -> whatsapp-worker/src/index.ts

import { Worker, Job } from 'bullmq';
import { sendTextMessage } from './whatsapp-cloud';

const worker = new Worker('whatsappQueue', async (job: Job) => {
    if (job.name === 'send-message') {
        const { userId, text } = job.data;
        const user = await prisma.user.findUnique({ where: { id: userId } });
        if (user?.phone) {
            await sendTextMessage(user.phone, text); // Appelle Graph API
        }
    }
}, { connection: redisConnection });

C. Bypass du blocage DNS (whatsapp-cloud.ts)

import axios from 'axios';
import https from 'https';
import dns from 'node:dns';

// Fix global ipv4first
dns.setDefaultResultOrder('ipv4first');

// Agent de secours
const httpsAgent = new https.Agent({ family: 4 });

export async function sendTextMessage(to: string, text: string): Promise<void> {
    const url = `https://graph.facebook.com/v18.0/${process.env.WHATSAPP_PHONE_NUMBER_ID}/messages`;
    await axios.post(url, body, {
        headers: { Authorization: `Bearer ${process.env.WHATSAPP_ACCESS_TOKEN}` },
        httpsAgent // Force le DNS à utiliser une route supportée
    });
}

D. Endpoints de Diagnostique DNS & Egress Hugging Face (api/src/index.ts)

server.get('/debug/net', async (_req, reply) => {
    const res = await fetch('https://www.google.com', { method: 'GET' });
    return reply.send({ ok: true, status: res.status }); // ✅ SUCCESS HF
});

server.get('/debug/graph', async (_req, reply) => {
    const res = await fetch('https://graph.facebook.com/v18.0', { method: 'GET' });
    return reply.send({ ok: true, status: res.status }); // ❌ ENOTFOUND HF
});