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).
- Héberge l'application
- 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.
- Héberge uniquement l'application
- Neon Postgres :
- Rôle : Base de données relationnelle centrale (Prisma) partagée entre HF et Railway (Tables
User,Track,Enrollment,TrackDay).
- Rôle : Base de données relationnelle centrale (Prisma) partagée entre HF et Railway (Tables
- 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.
- Rôle : Gestion de la file d'attente asynchrone BullMQ (queue
- 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).
- Rôle : LLM utilisé dans
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>.
- Utilisé par Meta une seule fois lors de la configuration avec
- 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-256en 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 DNSgraph.facebook.comfonctionne (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)dansapps/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é avecnew 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).
- Type
- 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'IPv4family: 4pour 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é
- 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)); - Rate Limiting Global (Hugging Face / Fastify)
Configuré dans
index.tsavec@fastify/rate-limit. Le seuil actuel est de300 requêtes / minutepar adresse IP (qui protégera l'API si le Webhook fuite mais n'impactera pas l'IP de Meta qui est distribuée). - API Key (Admin routes)
Vérification stricte de
Bearer <ADMIN_API_KEY>sur chaque route admin/backoffice. - CORS
CORS est ouvert globalement via
@fastify/corspour 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émaschema.prisma.
Commandes critiques (Railway Build & Start) :
- Root command (Installation) :
pnpm install - Génération client :
pnpm --filter @repo/database build(ouprisma 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 :
- Connect GitHub : Ajouter le repository Edtech à Railway.
- 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
- Root Directory : Laisser à
- 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, etWHATSAPP_ACCESS_TOKEN.
- Checklist de validation finale :
- Envoyer
INSCRIPTIONau 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.
- Envoyer
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
});