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 :**
```json
{
"Content-Type": "application/json",
"Authorization": "Bearer <WHATSAPP_ACCESS_TOKEN>"
}
```
* **Body Type "Text" :**
```json
{
"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`.
```typescript
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`
```typescript
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`
```typescript
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)
```typescript
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`)
```typescript
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
});
```