Ops & Security
La discipline
La différence entre un projet et un incident. Secrets, modèle de menace, audit adversarial et isolation entre projets. La discipline qui protège toute la galaxie.
OS-01 Secrets : public vs privé
leçon Décider, pour n'importe quelle clé ou token de tes projets, si elle peut vivre côté client ou si elle ne doit jamais quitter le serveur, et construire l'arbre de décision qui t'empêche de te tromper à nouveau.
Secrets : public vs privé
leçonDécider, pour n'importe quelle clé ou token de tes projets, si elle peut vivre côté client ou si elle ne doit jamais quitter le serveur, et construire l'arbre de décision qui t'empêche de te tromper à nouveau.
Une seule service_role key qui fuit dans un bundle Astro, c'est l'accès total à ta base de données en court-circuitant RLS. Ce n'est pas un bug qu'on corrige avec un patch : c'est une brèche qui t'oblige à faire tourner les identifiants, à auditer les accès et, s'il y a des données utilisateurs, à notifier. Le coût de l'erreur ici est le plus élevé de toute la galaxie.
LA LEÇON
Commence par la règle mentale qui ne te trahit jamais : tout ce qui est compilé dans un site statique est public. MOONKEY LAB, c'est Astro 4 en mode SSG — le build génère du HTML+JS brut servi depuis Cloudflare Pages. Il n'y a pas de serveur qui garde des secrets au runtime. N'importe quelle chaîne que tu importes dans un composant .astro et qui atteint le client finit, littéralement, dans un fichier que n'importe qui télécharge avec `view-source` ou en ouvrant DevTools → Sources. C'est pour ça que `src/lib/supabase.ts` ne contient QUE la anon key. La anon key est CONÇUE pour être publique : c'est un identifiant de projet + un token avec les permissions du rôle `anon`, dont l'autorité réelle est limitée par Postgres RLS. Ce n'est pas un secret ; c'est une adresse avec un portier.
Maintenant le contraste que tu dois intérioriser. Il existe deux clés Supabase qui se ressemblent (un long JWT) mais qui appartiennent à des univers de pouvoir opposés. La `anon` key opère sous le rôle `anon`/`authenticated` et est SOUMISE à RLS : elle ne voit que ce que les policies autorisent. La `service_role` key opère avec `BYPASSRLS` : elle ignore toutes les policies, lit et écrit n'importe quelle ligne de n'importe quelle table — y compris celles de XHUB IRON (`iron_*`, `world_*`) qui partagent la même instance `wuchsslgbqlhyxljsmxi`. Si la `service_role` apparaît côté client, un attaquant ne compromet pas seulement MOONKEY : il pivote vers tous les projets qui vivent dans cette DB. Règle dure : la `service_role` n'entre JAMAIS dans un repo de frontend, ni dans un `.astro`, ni dans un `import.meta.env.PUBLIC_*`. Sa place est un backend avec session (une Edge Function, un process serveur) ou ton trousseau local — jamais le bundle.
Le vecteur de fuite le plus courant, ce n'est pas de coller la clé dans le code : c'est le préfixe de variable d'environnement. Dans Astro, `import.meta.env.PUBLIC_X` est INLINÉ dans le bundle du client au build-time ; `import.meta.env.X` (sans `PUBLIC_`) n'existe que dans le contexte serveur/build et N'est PAS exposé. Vite/Astro font ça par design. L'erreur classique : nommer `PUBLIC_SUPABASE_SERVICE_KEY` 'pour que le composant la voie'. Tu y arrives — et tu l'offres au monde entier. Ton arbre de décision pour nommer une env var : cette clé peut-elle être publique ? Oui → `PUBLIC_`. Non → sans préfixe, et en plus demande-toi ce qu'elle fait dans un projet SSG (elle ne devrait probablement pas y être).
Construis l'inventaire. Ouvre ton projet et classe chaque identifiant dans un tableau à trois colonnes : nom · classe (publique/privée) · où elle vit aujourd'hui. Dans MOONKEY les entrées réelles sont : `PUBLIC_SUPABASE_URL` (publique, identifiant), `PUBLIC_SUPABASE_ANON_KEY` (publique, limitée par RLS), et la `service_role` (privée — elle NE doit PAS exister du tout dans ce repo ; si tu en as besoin pour un script de migration, elle vit dans ton shell local ou dans un secret de CI, jamais dans `src/`). Ajoute tout token tiers que tu touches : clés d'email transactionnel, webhooks, tokens d'analytics côté serveur. Chaque ligne privée qui est du mauvais côté est un incident qui attend de se produire.
Le contrôle qui ferme la boucle, c'est la détection automatique, parce que la discipline humaine défaille. Avant chaque commit tu veux que quelque chose hurle si une clé privée se glisse. Le pattern minimal : un grep en pre-commit qui cherche le pattern d'une `service_role` (les JWT de service Supabase portent `"role":"service_role"` dans le payload base64) et des mots comme `service_role`, `BEGIN PRIVATE KEY`, `sk-`. Pour les repos réels, utilise `gitleaks` ou `trufflehog` comme hook. Et si une clé a DÉJÀ fuité dans un commit passé : il ne suffit pas de l'effacer du code — elle reste dans l'historique git. Il faut la FAIRE TOURNER dans le panel de Supabase (Settings → API → roll key) et réécrire l'historique si c'était un vrai secret. Faire tourner est obligatoire ; effacer le fichier ne révoque rien.
Termine avec le cas Espejo, qui ajoute un axe éthique, pas seulement technique. Espejo est multi-tenant : chaque influenceur-tenant a ses données. Là, la question 'publique vs privée' se dédouble : en plus des clés d'infrastructure, tu manipules des données personnelles/consenties d'utilisateurs finaux. La clé de service capable de lire ACROSS tenants est le secret le plus sensible du produit — sa fuite n'est pas seulement technique, elle viole la frontière de données que la mémoire de projet marque comme non négociable. Leçon transversale : un identifiant privé ne se définit pas par son format, il se définit par ce qu'il déverrouille. Mesure-le toujours par son blast radius.
EXERCICE
Dans ton projet (utilise MOONKEY ou l'un des tiens) : 1) Crée `SECRETS.md` avec le tableau d'inventaire (nom · classe · emplacement · blast radius) pour chaque identifiant que le projet touche. 2) Exécute `grep -rn "service_role\|BEGIN.*PRIVATE KEY\|sk-" src/ .env* 2>/dev/null` et documente ce qu'il a trouvé (idéalement rien dans `src/`). 3) Écris un hook `.git/hooks/pre-commit` qui fait ce grep sur les fichiers staged (`git diff --cached --name-only`) et abandonne avec exit 1 s'il y a un match. 4) Vérifie le hook en essayant de committer un fichier de test contenant la chaîne `"role":"service_role"` et confirme qu'il le bloque.
LIVRABLE
`SECRETS.md` avec le tableau d'inventaire complet (chaque identifiant classé par classe et blast radius) + un hook `.git/hooks/pre-commit` exécutable qui bloque les commits contenant des secrets privés, testé contre un cas qui doit échouer.
CLÉ ESSENTIELLE
Un identifiant n'est pas secret par son apparence, mais par ce qu'il déverrouille : la anon key et la service_role sont toutes deux des JWT identiques à l'œil, et l'une est publique par design tandis que l'autre compromet toute la galaxie. Classe toujours par blast radius, jamais par format.
ERREURS À ÉVITER
- ×Mettre le préfixe `PUBLIC_` à une clé privée 'pour que le composant la lise' — Astro l'inline dans le bundle et la publie au monde entier.
- ×Croire qu'effacer la clé du fichier la révoque : elle reste dans l'historique git et reste active jusqu'à ce que tu la FASSES TOURNER dans le panel du fournisseur.
- ×Traiter la anon key comme un secret et l'obfusquer : tu perds ton temps à protéger quelque chose de public pendant que l'autorité réelle (RLS) reste sans révision.
- ×Supposer qu'un site SSG 'n'a aucun secret exposé' sans auditer le bundle généré : ce qui compte, c'est ce qu'il y a dans `dist/`, pas ce qu'il y a dans `src/`.
- ×Mettre la service_role dans un script de migration versionné dans le repo au lieu de la lire depuis le shell local ou depuis un secret de CI à l'exécution.
OS-02 Modèle de menace d'une SSG
leçon Penser comme l'attaquant d'un site statique avec anon key : énumérer la vraie surface d'attaque d'un SSG + Supabase et comprendre pourquoi la seule défense qui compte est RLS, pas le client.
Modèle de menace d'une SSG
leçonPenser comme l'attaquant d'un site statique avec anon key : énumérer la vraie surface d'attaque d'un SSG + Supabase et comprendre pourquoi la seule défense qui compte est RLS, pas le client.
Beaucoup de gens protègent la mauvaise porte : ils cachent le panel admin dans le frontend et laissent la DB ouverte. Un site statique n'a pas de serveur qui valide quoi que ce soit — si ton modèle de menace ne part pas de 'le client est hostile et contrôlé par l'attaquant', tu construiras des défenses décoratives qu'on contourne avec curl.
LA LEÇON
Adopte le cadre mental correct : dans un SSG, le client n'est pas ton application, c'est un territoire ennemi. L'attaquant a ton bundle complet (il est public), ton `PUBLIC_SUPABASE_URL`, ta anon key, et il sait que derrière il y a Postgres. Il n'a pas besoin de 'hacker' ton JavaScript : il le lit. Il peut instancier son propre client Supabase avec TA anon key depuis une console Node et parler directement à ta DB, sans jamais passer par ton HTML. Donc, toute logique de sécurité qui vit dans un `.astro` ou dans du JS client — un `if (user.role === 'admin')`, un composant qui se cache — c'est de l'UX, pas de la sécurité. Le CLAUDE.md de MOONKEY le dit littéralement : `admin.astro` est de l'UX ; la sécurité NE dépend PAS de cette barrière.
Énumère la surface d'attaque concrète. Pour MOONKEY il y a cinq tables atteignables avec la anon key : `profiles, progress, feedback, leads, proofs`. L'attaquant, avec un client authentifié via magic-link (n'importe qui peut s'inscrire), tentera l'évident : `supabase.from('profiles').select('*')` pour vider TOUS les profils ; `supabase.from('leads').select('*')` pour voler la liste de leads ; un `update` sur sa propre ligne de `profiles` en mettant `role = 'admin'` ou `founder_badge = true` pour s'auto-escalader ; un `insert` dans `proofs` avec un `user_id` étranger pour falsifier le progrès d'un autre. Chacune de ces actions est une hypothèse d'attaque que TOI tu dois réfuter avec une policy. Si tu ne l'as pas réfutée explicitement, suppose qu'elle fonctionne.
Comprends pourquoi RLS est l'autorité et comment se compose la vraie défense de MOONKEY. Le SELECT sur `profiles/progress/feedback/proofs` est self-or-admin : la policy autorise la ligne seulement si `user_id = auth.uid()` OU `is_admin()`. `is_admin()` est SECURITY DEFINER — elle s'exécute avec les permissions du propriétaire, consulte si l'appelant est admin sans que l'appelant puisse manipuler cette décision. Résultat : un non-admin qui demande `select('*')` ne reçoit pas une erreur, il reçoit SES lignes et rien d'autre — le dump massif renvoie une seule ligne, la sienne. L'escalade de privilèges est coupée par un trigger (`guard_privileged_profile_columns`) : `role` et `founder_badge` sont immuables pour les non-admins, donc l'`update role='admin'` est rejeté au niveau de la DB. L'insert étranger est coupé par `proofs_insert_self` : il exige une session et lie `user_id = auth.uid()`.
La conséquence opérationnelle du modèle : vérifie par impersonation, pas par lecture de code. Une policy qui 'a l'air' correcte peut avoir un trou (un OR mal placé, un USING sans WITH CHECK qui laisse passer des updates). La seule preuve qui vaille, c'est de te mettre dans les chaussures de l'attaquant : authentifie-toi comme un utilisateur normal et EXÉCUTE les attaques énumérées plus haut contre la vraie DB, en confirmant que chacune renvoie du vide ou une erreur. Le CLAUDE.md mentionne que l'isolation de lecture est 'vérifiée par impersonation' — c'est ça le standard. 'J'ai écrit la policy' n'est pas une preuve ; 'j'ai tenté l'attaque en tant qu'utilisateur X et il n'a pas lu la ligne de l'utilisateur Y' en est une.
N'oublie pas les vecteurs qui ne sont pas RLS. Le modèle de menace d'un SSG inclut aussi : (a) la différence USING vs WITH CHECK — USING filtre quelles lignes tu vois/affectes, WITH CHECK valide l'état résultant d'un INSERT/UPDATE ; oublier WITH CHECK dans un UPDATE te laisse déplacer une ligne vers un user_id étranger. (b) Les RPC : une fonction SECURITY DEFINER mal écrite est une brèche qui ignore RLS par design — MOONKEY les restreint à `authenticated` et révoque les opérations dangereuses. (c) TRUNCATE : révoqué de anon/authenticated, parce que RLS ne protège pas contre TRUNCATE (il efface la table entière sans évaluer les policies par ligne). (d) Fuite par erreurs et par colonnes : un SELECT qui renvoie une colonne sensible que tu as oublié d'exclure.
Atterris avec le contraste entre projets pour fixer le principe. XNLAB est un site de marque presque sans backend : sa surface est minimale — formulaires de contact, en-têtes, peut-être un endpoint de captation. Là le modèle de menace est 'spam et XSS sur le peu qui accepte de l'input', pas l'escalade de DB. XHUB IRON vit dans la MÊME DB que MOONKEY mais c'est un panel opérationnel du fondateur : ses tables `iron_*` ne doivent pas être lisibles par un utilisateur de MOONKEY — le même moteur RLS doit maintenir DEUX produits qui partagent Postgres sans que l'un lise l'autre (c'est OS-05). Le modèle de menace n'est pas générique : il se dérive de QUELLES données il y a, QUI peut s'authentifier, et QUOI partage l'infrastructure. Commence toujours par inventorier ces trois axes.
EXERCICE
Sur MOONKEY (ou ton projet Supabase) : 1) Écris `THREAT-MODEL.md` en listant, pour chaque table atteignable avec la anon key, les attaques qu'un utilisateur authentifié hostile tenterait (dump massif, escalade de privilèges, écriture sur une ligne étrangère). 2) Pour chaque attaque, note la défense qui DEVRAIT l'arrêter (quelle policy/trigger) et marque-la comme 'vérifiée' ou 'non vérifiée'. 3) Écris le snippet d'impersonation que tu exécuterais pour tester le dump de `profiles` en tant qu'utilisateur normal (client avec anon key + session d'un user de test faisant `.from('profiles').select('*')`) et prédis le résultat attendu (1 seule ligne).
LIVRABLE
`THREAT-MODEL.md` : un tableau attaque→défense→état-de-vérification qui couvre chaque table exposée par la anon key, plus le snippet d'impersonation concret pour au moins une attaque de dump massif avec son résultat attendu.
CLÉ ESSENTIELLE
Dans un SSG le client est le territoire de l'attaquant : il a ton bundle, ta anon key et parle directement à ton Postgres via curl sans toucher ton HTML. Toute vérification de sécurité qui vit dans un .astro est de l'UX ; la seule frontière réelle est RLS, et elle ne compte que lorsque tu l'as réfutée par impersonation, pas par lecture.
ERREURS À ÉVITER
- ×Confondre cacher le panel admin dans le frontend avec le protéger : `admin.astro` cache l'UI, mais la DB reste accessible via curl avec la anon key.
- ×Vérifier les policies en les lisant au lieu de les attaquer en utilisateur impersonné — un OR mal placé ne se voit qu'en exécutant l'attaque.
- ×Écrire un UPDATE avec USING mais sans WITH CHECK, permettant de réassigner la ligne à un `user_id` étranger même si la policy 'a l'air' de protéger.
- ×Oublier que TRUNCATE et les RPC SECURITY DEFINER contournent RLS : les protéger exige un REVOKE explicite, avoir des policies de ligne ne suffit pas.
- ×Appliquer un modèle de menace générique copié d'un tutoriel au lieu de le dériver de tes trois axes réels : quelles données, qui s'authentifie, quelle infrastructure est partagée.
OS-03 Audit adversarial
leçon Auditer de façon adversariale : traiter chaque trouvaille de sécurité comme une hypothèse que tu dois réfuter avant d'y croire, au lieu d'accepter 'ça a l'air vulnérable' ou 'ça a l'air sûr' par inspection.
Audit adversarial
leçonAuditer de façon adversariale : traiter chaque trouvaille de sécurité comme une hypothèse que tu dois réfuter avant d'y croire, au lieu d'accepter 'ça a l'air vulnérable' ou 'ça a l'air sûr' par inspection.
Le biais de confirmation ruine les audits : un scanner dit 'RLS désactivé' et tu paniques, ou toi tu dis 'la policy a l'air bien' et tu signes le feu vert. Les deux erreurs coûtent cher — les faux positifs brûlent ton temps et ta crédibilité ; les faux négatifs laissent des brèches ouvertes. La discipline de réfuter d'abord est ce qui sépare un vrai audit d'une opinion.
LA LEÇON
Le principe directeur est poppérien : une trouvaille de sécurité n'est pas vraie parce que tu l'affirmes, elle est vraie parce que tu as TENTÉ de la réfuter et que tu n'as pas pu. Inverse le flux naturel. Quand tu crois avoir trouvé une vulnérabilité ('n'importe qui peut lire `leads`'), ton étape suivante n'est pas de la reporter — c'est d'essayer de démontrer que tu te trompes : y a-t-il une policy qui l'empêche ? l'ai-je testée avec le bon rôle ? le `select` a-t-il renvoyé des données réelles ou du vide ? Ce n'est que lorsque ta tentative de réfutation échoue que la trouvaille s'élève au rang de confirmée. Et inversement : quand tu crois que quelque chose est sûr ('la policy self-or-admin protège `profiles`'), ton travail est de l'attaquer jusqu'à ce qu'elle se brise ou survive.
Définis les deux erreurs que tu poursuis avec des noms clairs. Faux positif : tu reportes une vulnérabilité qui n'existe pas — par exemple tu cries 'la anon key est exposée dans le bundle' alors que la anon key est publique par design et que la DB est protégée par RLS (voir OS-01). Tu brûles la confiance ; la prochaine fois personne ne te croira. Faux négatif : tu tiens pour sûr quelque chose qui ne l'est pas — par exemple 'le panel admin est caché, tout va bien' sans tester l'accès direct à la DB. Tu laisses la brèche ouverte. L'audit adversarial existe pour minimiser les DEUX, et l'outil pour les deux est le même : la preuve exécutable.
La méthode concrète : pour chaque trouvaille, produis une preuve reproductible, pas une affirmation. 'Reproductible' signifie : une commande ou un snippet que n'importe qui peut lancer et voir le même résultat. Pour confirmer que `profiles` NE peut PAS être dumpé : un script qui s'authentifie en user_A, fait `.from('profiles').select('*')`, et imprime le compte de lignes — attendu 1 (seulement la sienne). Pour confirmer que l'escalade de privilèges est fermée : en user_A, `.from('profiles').update({ role: 'admin' }).eq('id', myId)` et imprime l'erreur que renvoie le trigger guard. Si l'attaque renvoie ce que tu attendais (vide/erreur), le contrôle est vérifié. Si elle renvoie des données, tu as une trouvaille RÉELLE — et elle arrive déjà avec son PoC joint.
Travaille avec l'outil de Supabase lui-même mais sans t'y rendre. `get_advisors` (security lints) te signale les tables sans RLS, les policies permissives, les fonctions SECURITY DEFINER sans search_path fixé. Traite-le comme un générateur d'HYPOTHÈSES, pas de verdicts. Un advisor qui dit 'la fonction X est SECURITY DEFINER mutable search_path' est une hypothèse de risque : va à la fonction, regarde si un attaquant peut planter un objet dans un schema qu'elle résout en premier. Parfois c'est exploitable, parfois non selon le contexte. L'advisor t'épargne le balayage initial ; la réfutation, c'est toi qui la fais. Le CLAUDE.md l'institutionnalise déjà : changements de RLS = migration versionnée + `get_advisors` ensuite. Cette seconde partie est l'audit adversarial transformé en routine.
Surveille les faux négatifs de ta propre méthode : l'attaque qui 'passe' peut passer pour la mauvaise raison. Si ton `select('*')` sur `profiles` renvoie du vide, est-ce parce que RLS l'a bloqué, ou parce que ta session de test a expiré / tu n'étais pas authentifié / la table était vide ? Un contrôle ne se valide jamais sans un cas de CONTRASTE : prouve que la même query renvoie BIEN des données quand elle le doit (en tant qu'admin, ou en tant que propriétaire de la ligne). Une preuve de sécurité sans son contrôle positif est un faux négatif déguisé en succès. Vérifie toujours que ton arme fonctionne avant de conclure que la cible est immunisée.
Atterris la rigueur dans les projets où se tromper fait le plus mal. Dans XCAP l'audit adversarial est une doctrine centrale : la mémoire du projet le décrit comme 'anti-fabrication par construction' — un système qui enregistre des forecasts et les calibre contre des résultats réels, réfutant ses propres prédictions au lieu d'y croire. Ce même muscle s'applique à la sécurité : ne signe pas 'le ledger est read-only' tant qu'une tentative d'écriture avec l'identifiant d'ingest n'échoue pas sous tes yeux. Dans Espejo, où la frontière de données personnelles est éthique en plus de technique, un audit qui dit 'pas de fuite entre tenants' sans un PoC d'un tenant tentant de lire un autre N'EST PAS un audit : c'est un vœu. Réfuter d'abord, toujours, surtout quand le résultat que tu attends est le confortable.
EXERCICE
Prends un contrôle de sécurité réel de MOONKEY (par exemple 'un non-admin ne peut pas lire les profils étrangers'). 1) Formule-le comme une hypothèse réfutable. 2) Écris DEUX preuves : celle de l'attaque (en user_A je tente de lire la ligne de user_B → j'attends du vide) et celle du contraste (en user_A je lis MA ligne → j'attends 1 ligne ; ou en admin je lis toutes → j'attends N). 3) Exécute `get_advisors` (ou documente ce qu'il signalerait) et pour chaque lint classe-le comme 'hypothèse confirmée comme risque réel' ou 'réfutée (non exploitable parce que…)'. 4) Écris un mini-rapport où chaque affirmation est accompagnée de sa commande reproductible.
LIVRABLE
`AUDIT.md` : un rapport où chaque trouvaille (vulnérable ou sûre) porte son PoC reproductible — attaque + cas de contraste positif — et où chaque lint de `get_advisors` est marqué comme confirmé ou réfuté avec sa raison.
CLÉ ESSENTIELLE
Une preuve de sécurité qui 'passe' sans cas de contraste positif est un faux négatif déguisé : le select vide peut être dû à RLS ou à une session expirée. Vérifie toujours que ton arme tire avant de déclarer la cible immunisée — réfuter d'abord s'applique aussi à ta propre méthode.
ERREURS À ÉVITER
- ×Reporter une trouvaille par inspection de code sans PoC exécutable : 'la policy a l'air faible' n'est pas une trouvaille, c'est une intuition.
- ×Traiter les lints de `get_advisors` comme des verdicts au lieu d'hypothèses à réfuter — ils génèrent des faux positifs qui, répétés, brûlent ta crédibilité.
- ×Tenir pour sûr un contrôle parce que l'attaque a renvoyé du vide, sans prouver que la même query renvoie BIEN des données quand elle le doit (contrôle positif).
- ×Crier 'anon key exposée' comme vulnérabilité alors qu'elle est publique par design : le faux positif classique qui trahit qu'on n'a pas compris le modèle de sécurité.
- ×Auditer seulement dans la direction confortable (confirmer ce qu'on attend) au lieu d'attaquer tout aussi fort les contrôles qu'on croit sûrs.
OS-04 Incident response & rollback
leçon Répondre à un incident en production avec une procédure froide : détecter, contenir, faire un rollback de façon sûre, et conduire un post-mortem sans blâme qui ferme la cause racine.
Incident response & rollback
leçonRépondre à un incident en production avec une procédure froide : détecter, contenir, faire un rollback de façon sûre, et conduire un post-mortem sans blâme qui ferme la cause racine.
Quand quelque chose casse en production, l'instinct (improviser, toucher la DB à chaud, cacher l'erreur) ne fait souvent qu'aggraver les dégâts. La différence entre une frayeur de dix minutes et un week-end perdu, c'est d'avoir un runbook écrit AVANT l'incident. Sans lui, un rollback mal fait détruit des données que le bug n'avait fait que corrompre.
LA LEÇON
Intériorise la séquence DCRP : Détecter → Contenir → Récupérer (rollback) → Post-mortem. L'erreur de la plupart est de sauter directement à 'réparer', en touchant la production à l'aveugle. L'ordre importe parce que chaque phase protège la suivante : si tu détectes mal, tu contiens ce qui n'est pas le problème ; si tu ne contiens pas, les dégâts grandissent pendant que tu récupères ; si tu récupères sans comprendre, tu répètes l'incident. Et la règle zéro, écrite dans tout runbook sérieux : pendant un incident, tu N'improvises PAS sur la production. Tu exécutes des étapes prédéfinies. La créativité est pour le post-mortem, pas pour 3h du matin avec le site à terre.
Détecter, c'est savoir QUOI est cassé et depuis QUAND. Dans MOONKEY (Cloudflare Pages + Supabase) tes signaux sont : le deploy de Cloudflare (quel commit est live ? le dernier build est-il passé ?), les logs de Supabase (`get_logs` par service : api, postgres, auth — c'est là que tu vois les erreurs de policy, les queries qui échouent, les pics de 4xx/5xx), et la plainte de l'utilisateur (souvent le premier signal). La première chose que tu établis, c'est la chronologie : qu'est-ce qui a changé juste avant ? Presque toujours l'incident corrèle avec un deploy ou une migration récente. 'Ça a commencé après le merge de 16h20' réduit l'espace de recherche d'heures à minutes.
Contenir, c'est arrêter l'hémorragie avant de soigner la plaie. Question clé : est-ce que ça corrompt des données EN CE MOMENT ? Si un bug écrit de mauvaises lignes dans `proofs` ou `progress`, chaque minute de retard multiplie le nettoyage. Options de contention selon la sévérité : revenir au deploy précédent de Cloudflare (un clic — Cloudflare Pages garde les deploys antérieurs et permet un rollback instantané, c'est ton levier le plus rapide et le plus sûr), désactiver la feature cassée, ou dans le cas extrême d'écriture destructrice, couper l'accès au niveau de la policy. Contenir N'est PAS réparer : c'est figer les dégâts dans leur état actuel pour avoir le temps de réfléchir.
Récupérer exige de distinguer DEUX rollbacks que les gens confondent et qui ont des risques opposés. (1) Rollback de CODE : revenir à un deploy/commit antérieur. C'est bon marché, réversible et presque toujours sûr — dans Cloudflare Pages c'est immédiat. C'est ta première option. (2) Rollback de SCHEMA/DONNÉES : revenir sur une migration ou restaurer des données. C'est DANGEREUX et irréversible : une migration `down` qui fait `DROP COLUMN` efface des données qui n'étaient peut-être que corrompues, pas perdues. Règles : ne lance jamais un rollback de données sans un backup/PITR confirmé d'abord ; préfère une migration FORWARD qui corrige (un nouveau `UPDATE` qui répare les mauvaises lignes) à un `down` destructeur ; toute migration Supabase est versionnée (`apply_migration`) précisément pour que l'état soit reconstructible. Si le doute est 'rollback de code ou de données', commence TOUJOURS par celui de code : souvent il suffit et ne touche pas la DB.
Le post-mortem est l'endroit où l'incident rembourse sa dette en se transformant en apprentissage. Il est SANS BLÂME (blameless) : l'objectif n'est pas de savoir qui a cassé, mais ce qui dans le SYSTÈME a permis qu'un humain normal casse. Structure : timeline (quoi s'est passé et quand, minute par minute), impact (combien d'utilisateurs, quelles données, combien de temps), cause racine (les '5 pourquoi' jusqu'à arriver à la défaillance de système, pas de personne), et actions correctives avec responsable et date. Chaque incident doit produire au moins un contrôle qui l'aurait PRÉVENU ou DÉTECTÉ plus tôt — un test, un advisor en CI, une alerte. Si le post-mortem ne change rien au système, ce n'était pas un post-mortem, c'était une confession.
Ancre le runbook dans la réalité de chaque projet, parce que la sévérité et les leviers changent. Dans XCAP existe une invariante de capital sacrée — le capital s'accumule entre sessions et ne bouge qu'aux clôtures réalisées, ne se réinitialise jamais. Un incident qui toucherait cette invariante NE se répare PAS avec un rollback de données à l'aveugle : tu effacerais de l'histoire comptable réelle. Là le runbook oblige à une correction forward et à la vérification de l'invariante avant de toucher quoi que ce soit. Dans Espejo, multi-tenant, un incident a une question en plus dans la phase de détection : la défaillance a-t-elle traversé la frontière entre tenants ? Si un bug a exposé les données d'un tenant à un autre, en plus du rollback il y a obligation de contenir la fuite. Le runbook générique te donne la colonne vertébrale ; les invariantes de chaque projet te donnent les règles que tu ne violes JAMAIS pendant la panique.
EXERCICE
Écris le `RUNBOOK.md` d'incidents pour MOONKEY : 1) La section Détecter (quelles commandes/panels tu regardes : deploy de Cloudflare, `get_logs` de Supabase par service, comment tu établis la timeline). 2) La section Contenir avec le levier de rollback de Cloudflare Pages pas à pas. 3) Un arbre de décision 'rollback de code vs rollback de données' avec la règle 'commence par le code, données seulement avec PITR confirmé'. 4) Un template de post-mortem blameless (timeline / impact / 5-pourquoi / actions correctives avec responsable). 5) Simule un incident : 'après le deploy de 16h20, les utilisateurs ne peuvent plus sauvegarder de proofs' et écris la réponse pas à pas en suivant ton propre runbook.
LIVRABLE
`RUNBOOK.md` avec les quatre phases DCRP opérationnalisées pour Cloudflare Pages + Supabase, l'arbre de décision code-vs-données, un template de post-mortem blameless, et un incident simulé résolu de bout en bout avec ta propre procédure.
CLÉ ESSENTIELLE
Il y a deux rollbacks aux risques opposés : celui de code est bon marché et réversible (commence toujours par là), celui de données est destructeur et irréversible. En cas de doute, une migration forward qui répare les lignes corrompues est presque toujours meilleure qu'un `down` qui les efface — parce que souvent les données étaient endommagées, pas perdues.
ERREURS À ÉVITER
- ×Sauter directement à 'réparer' en touchant la production à l'aveugle au lieu de suivre Détecter→Contenir→Récupérer : l'improvisation à 3h du matin, c'est comme ça qu'une frayeur devient un désastre.
- ×Faire un rollback de données (down migration avec DROP) sans backup/PITR confirmé, détruisant des données que le bug n'avait fait que corrompre.
- ×Ne pas établir la timeline avant d'agir : sans savoir quel deploy ou migration a déclenché l'incident, tu cherches la cause dans tout le système au lieu des derniers changements.
- ×Confondre contenir avec réparer : laisser le bug écrire de mauvaises lignes pendant que tu 'enquêtes' multiplie le nettoyage ultérieur.
- ×Clôturer l'incident avec un post-mortem qui cherche un coupable au lieu d'une défaillance de système, et qui ne produit pas un seul contrôle nouveau qui l'aurait prévenu.
OS-05 Isolation entre projets
leçon Faire que plusieurs projets partagent une seule base de données Postgres sans qu'aucun puisse lire, écrire ou casser les données d'un autre — isolation multi-projet et multi-tenant imposée par la DB, pas par convention.
Isolation entre projets
leçonFaire que plusieurs projets partagent une seule base de données Postgres sans qu'aucun puisse lire, écrire ou casser les données d'un autre — isolation multi-projet et multi-tenant imposée par la DB, pas par convention.
Partager une instance Supabase entre projets fait économiser du coût mais crée le risque le plus silencieux de la galaxie : que le client de MOONKEY, avec sa anon key, lise les tables de XHUB IRON. L'isolation par 'souviens-toi de ne pas toucher ces tables' c'est du papier ; un jour quelqu'un les touche. La vraie isolation est structurelle et se vérifie en l'attaquant.
LA LEÇON
Pars du fait concret : l'instance `wuchsslgbqlhyxljsmxi` héberge DEUX produits. MOONKEY utilise `profiles, progress, feedback, leads, proofs`. XHUB IRON utilise `iron_*, world_*, focus_*, daily_focus_history`. Ils partagent le même Postgres, le même `auth.users`, et — c'est la clé — la MÊME anon key sert à parler à la DB. Ça veut dire que le client public de MOONKEY PEUT techniquement tenter `from('iron_command').select('*')`. La seule raison pour laquelle ça ne marche pas doit être une barrière dure dans la DB, pas la note de CLAUDE.md qui dit 'n'y touche pas'. Cette note protège contre le fait que TOI tu les édites par erreur en écrivant du code ; elle ne protège pas contre un attaquant qui a déjà ta anon key.
Comprends les couches d'isolation de la plus faible à la plus forte. (1) Convention (noms `iron_*` vs sans préfixe) : ça organise, ça n'isole pas — un attaquant ignore les conventions. (2) RLS par table : chaque table de XHUB doit avoir des policies qui n'accordent l'accès qu'à son propre rôle/utilisateur ; une table `iron_*` sans RLS ou avec une policy permissive est une fuite directe. (3) Schemas séparés : mettre XHUB dans son propre schema Postgres et NE PAS l'exposer dans l'API de PostgREST (le `db.schema` que Supabase publie) est une barrière plus forte — ce qui n'est pas dans le schema exposé, le client ne le voit même pas. (4) Rôles/grants : REVOKE de `anon`/`authenticated` sur les tables/objets étrangers. La défense robuste EMPILE ces couches ; ne te fie pas à une seule.
Pour le cas MOONKEY↔XHUB, la question d'audit est directe : un utilisateur authentifié de MOONKEY peut-il lire une ligne de `iron_*` ? Réfute-le (OS-03) : authentifie-toi en user normal de MOONKEY et exécute `supabase.from('iron_command').select('*')`. Le correct est que ça renvoie une erreur de permission ou du vide par RLS. Si ça renvoie des données, tu as une fuite d'isolation critique. La mémoire de projet dit que la santé est calculée et que le modèle est 'open RLS, computed health' pour le HUB — ça veut dire que tu dois vérifier EXPLICITEMENT que 'open' pour le HUB ne veut pas dire 'ouvert aux clients de MOONKEY'. Isolation entre projets = chaque table ne répond qu'à son propriétaire légitime, prouvé par impersonation croisée.
Le second axe, c'est le multi-tenant À L'INTÉRIEUR d'un projet, c'est Espejo. Ici ce ne sont pas deux produits, ce sont N tenants (chaque influenceur) dans les mêmes tables. L'isolation devient par-ligne : chaque ligne porte un `tenant_id`, et la policy RLS exige que `tenant_id` coïncide avec le tenant de l'appelant. L'attaque à réfuter : le tenant A tente de lire/écrire des lignes avec le `tenant_id` de B. Le piège classique, c'est l'UPDATE sans WITH CHECK : USING limite quelles lignes A voit, mais sans WITH CHECK, A pourrait réassigner une ligne à B ou en créer une avec un `tenant_id` étranger. Autre piège : une RPC SECURITY DEFINER qui reçoit `tenant_id` en paramètre et ne valide pas qu'il coïncide avec `auth.uid()` — brèche qui traverse tous les tenants. L'isolation multi-tenant n'est réelle que si CHAQUE policy de chaque opération (SELECT/INSERT/UPDATE/DELETE) ancre le tenant à l'identité de l'appelant, pas à un paramètre que le client contrôle.
Il y a des vecteurs de croisement qui ne sont pas SELECT et que les gens oublient. `auth.users` est partagé : un utilisateur est le même dans MOONKEY et dans XHUB — assure-toi qu'avoir un compte dans l'un n'accorde pas de rôles dans l'autre (dans MOONKEY, `role` et `founder_badge` sont immuables pour les non-admins précisément pour que s'inscrire n'escalade pas de privilèges croisés). Les RPC et fonctions SECURITY DEFINER sont des objets globaux : une fonction de XHUB exécutable par `authenticated` est invocable depuis le client de MOONKEY — restreins-la par rôle ou valide le contexte à l'intérieur. Triggers et séquences partagés, vues qui font un JOIN entre les tables des deux produits, et `get_advisors` qui signale un RLS manquant sur n'importe quelle table du projet : tout ça est une surface de croisement. L'isolation n'est pas une policy, c'est une propriété du système entier.
Termine avec la discipline opérationnelle qui maintient l'isolation vivante dans le temps. Chaque changement de schema entre comme migration versionnée (`apply_migration`), jamais comme édition manuelle dans le panel — ainsi l'état de sécurité est reconstructible et auditable. Après chaque migration qui touche des tables ou des policies, lance `get_advisors` pour chasser un RLS désactivé ou des policies permissives introduites par inadvertance. Et maintiens un test d'isolation comme partie du runbook : un script qui, en utilisateur de MOONKEY, tente de lire chaque préfixe étranger (`iron_*`, `world_*`) et fait échouer le build si l'un d'eux renvoie des lignes. L'isolation entre projets ne se 'configure pas une fois' ; elle se re-vérifie à chaque changement, parce qu'une migration innocente peut ouvrir une porte fermée depuis des mois.
EXERCICE
Sur la DB partagée `wuchsslgbqlhyxljsmxi` (ou une réplique de test) : 1) Liste dans `ISOLATION.md` les tables de chaque produit (MOONKEY vs XHUB) et la couche d'isolation qui protège chacune (RLS / schema non exposé / REVOKE). 2) Écris le test d'impersonation croisée : en utilisateur normal de MOONKEY, tente `select('*')` sur `iron_command` (ou n'importe quel `iron_*`/`world_*`) et documente le résultat attendu (erreur/vide). 3) Pour le cas multi-tenant d'Espejo, écris les quatre policies (SELECT/INSERT/UPDATE/DELETE) d'une table avec `tenant_id`, en t'assurant que UPDATE porte un WITH CHECK et qu'aucune ne fait confiance à un `tenant_id` passé par le client. 4) Documente comment `get_advisors` entre dans ta routine post-migration.
LIVRABLE
`ISOLATION.md` : la carte tables→couche-d'isolation pour les deux produits de la DB partagée, un test exécutable d'impersonation croisée MOONKEY→XHUB avec résultat attendu, et le set de quatre policies RLS par-tenant d'Espejo avec un WITH CHECK correct sur les opérations d'écriture.
CLÉ ESSENTIELLE
Partager Postgres entre projets n'est sûr que lorsque l'isolation est structurelle (RLS + schema non exposé + REVOKE), pas documentaire : la note 'ne touche pas à ces tables' protège contre tes erreurs de code, jamais contre un attaquant qui a déjà ta anon key et énumère les préfixes étrangers.
ERREURS À ÉVITER
- ×Confier l'isolation à des conventions de nommage ou à une note dans CLAUDE.md : un attaquant avec la anon key ignore les deux et essaie `from('iron_*').select('*')` directement.
- ×Laisser une table étrangère (`iron_*`, `world_*`) sans RLS ou avec une policy permissive dans une DB dont la anon key est partagée par plusieurs projets — fuite directe entre produits.
- ×En multi-tenant, écrire UPDATE/INSERT avec USING mais sans WITH CHECK, permettant à un tenant de réassigner ou créer des lignes avec le `tenant_id` d'un autre.
- ×Construire une RPC SECURITY DEFINER qui fait confiance à un `tenant_id` (ou `user_id`) passé en paramètre par le client au lieu de le dériver de `auth.uid()`.
- ×Traiter l'isolation comme une configuration unique et ne pas relancer `get_advisors` après chaque migration : un changement innocent peut rouvrir une porte fermée il y a des mois.
OS-06 Observabilidad y coste
leçon Monter un panel d'observabilité et un budget avec alertes pour un système vivant (Astro sur Cloudflare Pages + Supabase) : définir quels logs et métriques surveiller, ce qui déclenche une alerte, et quels plafonds d'usage et de dépense en tokens t'avertissent AVANT que la facture surprise n'arrive.
Observabilidad y coste
leçonMonter un panel d'observabilité et un budget avec alertes pour un système vivant (Astro sur Cloudflare Pages + Supabase) : définir quels logs et métriques surveiller, ce qui déclenche une alerte, et quels plafonds d'usage et de dépense en tokens t'avertissent AVANT que la facture surprise n'arrive.
Lancer n'est pas terminer : c'est le moment où tu commences à opérer. Un système en production génère trois flux que presque personne ne regarde jusqu'à ce qu'ils fassent mal : logs (qu'est-ce qui se passe), erreurs (qu'est-ce qui casse sans que personne te le dise) et coût (combien on te facture pour rester en vie). MOONKEY LAB, XHUB, Espejo et XCAP partagent une infrastructure réelle avec des plans free/pro et des quotas réels de Supabase et Cloudflare, et tout l'usage d'IA est facturé par token. L'opérateur qui ne monitore pas découvre les problèmes par deux voies, toutes deux chères : un utilisateur qui se plaint, ou une charge sur la carte. L'observabilité transforme 'je ne sais pas ce qui se passe en production' en 'j'ai un tableau de bord qui me le dit'. Le coût est de la sécurité : un endpoint ouvert que quelqu'un martèle n'est pas seulement un trou, c'est une facture. C'est la dernière discipline de la constellation parce qu'elle ferme la boucle : tu as construit, sécurisé, maintenant tu opères les yeux ouverts.
LA LEÇON
Les trois flux d'un système vivant. Logs = le journal de ce qui se passe (requêtes, queries, auth). Erreurs = ce qui casse en silence côté client, là où tes logs de serveur n'arrivent pas. Coût = le compteur qui tourne même si personne n'utilise le système. Ce sont trois tuyaux distincts avec trois outils distincts ; les confondre, c'est pourquoi les gens croient monitorer alors qu'ils ne le font pas.
L'observabilité dans un SSG n'est pas la même chose que dans un serveur. MOONKEY est statique : il n'y a PAS de handler côté serveur où mettre un logger. Ta télémétrie vit dans trois endroits étrangers à ton HTML : (1) Cloudflare Pages Analytics (requests, bande passante, erreurs d'edge), (2) les logs de Supabase (Dashboard → Logs : API, Postgres, Auth — chaque query qui passe par PostgREST et chaque login laisse une trace), et (3) le navigateur de l'utilisateur (erreurs de JS que toi seul ne vois pas). Accepte-le : dans un SSG, une grande partie de ce qui défaille se produit sur une machine que tu ne contrôles pas.
Quoi surveiller vraiment, pas tout. La tentation est de tout logguer et de ne rien lire. Définis une poignée de signaux qui comptent : taux d'erreur 4xx/5xx sur l'edge (un pic de 401/403 = RLS qui rejette ou un attaquant qui teste), erreurs d'Auth dans Supabase (tentatives échouées de magic-link), latence des queries lentes (Supabase marque celles qui tardent), et volume de lignes lues (une query sans filtre qui renvoie soudain 10 000 lignes est un bug de coût). Une métrique qui ne déclenche aucune décision est du bruit ; efface-la.
Error tracking côté client : l'angle mort. Dans un site statique, un TypeError dans ton JS casse l'expérience de l'utilisateur et toi tu ne reçois RIEN — ni log de serveur ni alerte. Tu dois capturer les erreurs dans le navigateur et les envoyer dehors. Le minimum honnête : un window.addEventListener('error', ...) et ('unhandledrejection', ...) qui fait un fetch vers un endpoint à toi (une Edge Function de Supabase qui insère dans une table de logs avec un RLS d'insert seulement). Le robuste : Sentry ou similaire avec son SDK gratuit jusqu'à un certain volume. La règle : si tu ne captures pas les erreurs client, tu opères à l'aveugle sur la moitié de ton système.
Le coût réel d'opérer, décomposé. Le free tier de Supabase a des plafonds concrets qui, une fois franchis, te coupent ou te facturent : lignes de DB, bande passante d'egress, stockage, Monthly Active Users d'Auth, et invocations d'Edge Functions. Cloudflare Pages est généreux mais a des limites de builds/mois et de requests dans les fonctions. Et la dépense qui escalade le plus et se voit le moins : les tokens d'IA. Chaque appel à l'API de Claude est facturé par tokens d'entrée et de sortie, avec des tarifs distincts ; un prompt avec un CLAUDE.md géant ou une boucle d'agent sans plafond peut multiplier le coût sans que change 'ce que fait' l'app. L'opérateur connaît ses trois factures : DB, edge et tokens.
Pourquoi les factures surprises arrivent, et comment ne pas les recevoir. Trois causes classiques : (1) un endpoint ou une Edge Function sans rate-limit que quelqu'un découvre et martèle — usage = argent ; (2) une query sans LIMITE ni index qui scanne toute la table à chaque chargement ; (3) une boucle d'agent IA sans plafond de tokens ni d'itérations (le capital invariant de XCAP existe en partie pour ça : des boucles qui accumulent de l'apprentissage, pas du coût). La défense n'est pas de surveiller la facture en fin de mois, c'est de mettre les freins AVANT : budgets avec alerte à 50/80/100 %, limites dures là où la plateforme les offre, et plafonds explicites de max_tokens et d'itérations dans chaque appel d'IA.
Alerter sur ce qui compte, sans fatigue d'alertes. Une alerte qui se déclenche toutes les heures est ignorée au bout d'une semaine. Le critère : n'alerte que sur l'actionnable et l'urgent. Bon : 'la dépense du mois a dépassé 80 % du budget', 'le taux de 5xx a dépassé 1 %', 'erreurs client > X en 10 min'. Mauvais : 'il y a eu une requête'. Configure les budget alerts que te donnent déjà Supabase, Cloudflare et la console d'Anthropic — ils sont gratuits et c'est ta première ligne. Chaque alerte doit répondre à 'que fais-je quand elle se déclenche' ; s'il n'y a pas d'action, ce n'est pas une alerte, c'est de l'anxiété.
Ferme la boucle avec la réponse aux incidents (OS-04). Observer sans plan de réaction, c'est juste regarder brûler. Le panel et les alertes de ce module sont les capteurs ; le rollback et l'incident response d'OS-04 sont les actionneurs. Un système bien opéré connecte les deux : l'alerte te réveille, les logs te disent ce qui s'est passé, et le plan de réponse te dit quoi faire. Observabilité + coût + réponse = opérer pour de vrai, pas prier.
EXERCICE
Tu opères MOONKEY LAB (Astro SSG sur Cloudflare Pages + Supabase moonkey-lab + appels d'IA possibles). Monte son observabilité réelle et son budget en quatre étapes. ÉTAPE 1 — Carte de signaux (livrable écrit). Ouvre le Dashboard de Supabase → Logs et examine les trois streams (API, Postgres, Auth). Documente dans un tableau quel signal tu surveilles dans chacun (ex. Auth : taux de magic-links échoués ; API : ratio de 401/403 ; Postgres : queries > 500ms), et pour chacun : seuil, ce que signifie le franchir, et quelle action il déclenche. Minimum 6 signaux. Un sans action est écarté. ÉTAPE 2 — Capture d'erreurs client. Implémente un script global (dans Layout.astro, à côté de l'IntersectionObserver déjà global — ne le duplique pas) avec window.addEventListener('error') et ('unhandledrejection') qui fait un fetch vers une Edge Function de Supabase. Crée l'Edge Function et une table client_errors avec un RLS d'insert SEULEMENT pour anon (jamais de SELECT pour anon : les logs ne se lisent pas depuis le client). Applique-la comme migration versionnée et lance get_advisors ensuite (règle de coût du projet). Provoque une erreur volontairement et vérifie que la ligne apparaît dans la table. ÉTAPE 3 — Les trois factures et leurs plafonds. Documente les limites actuelles et où on les voit : (a) Supabase — usage de DB, egress, Auth MAU, invocations de fonctions (Dashboard → Reports/Usage) ; (b) Cloudflare — builds et requests (Pages → Analytics) ; (c) tokens d'IA — s'il y a des appels à l'API de Claude, calcule le coût d'un appel typique (tokens d'entrée du CLAUDE.md + prompt, tokens de sortie attendus, au tarif en vigueur du modèle). Pour chaque facture, note le plafond gratuit et à quel % tu es aujourd'hui. ÉTAPE 4 — Freins et alertes. Active les budget alerts dans la console d'Anthropic et dans Supabase (ou documente exactement où ils se configurent si ton plan ne les expose pas). Pour tout appel d'IA dans le système, fixe un max_tokens explicite et un plafond d'itérations s'il s'agit d'une boucle. Livre un runbook d'une page : 'quand l'alerte X se déclenche → regarde le log Y → exécute l'action Z (qui se relie au rollback d'OS-04)'. Ne livre pas de captures de 'ça a l'air de marcher'. Livre : le tableau de signaux, l'Edge Function + migration qui tourne avec une ligne d'erreur réelle capturée, le détail des trois factures avec pourcentages, et le runbook qui connecte alerte → log → action.
LIVRABLE
Un panel d'observabilité opérationnel pour MOONKEY LAB avec : (1) un tableau de 6+ signaux (seuil · signification · action) sur les logs réels de Supabase et Cloudflare ; (2) une capture d'erreurs client fonctionnelle — Edge Function + table client_errors avec RLS insert-seulement, appliquée comme migration versionnée et vérifiée avec une ligne d'erreur réelle ; (3) le détail des trois factures (Supabase, Cloudflare, tokens d'IA) avec le plafond gratuit et le % d'usage actuel de chacune ; et (4) un runbook d'une page avec des budget alerts actifs qui connecte chaque alerte à son log et à son action de réponse (en se reliant au rollback d'OS-04).
CLÉ ESSENTIELLE
Dans un SSG il n'y a pas de serveur où logguer, donc la moitié de ce qui défaille se produit dans le navigateur de l'utilisateur et ne t'arrive jamais : si tu ne captures pas les erreurs client, tu ne monitores pas, tu devines. Et le coût est une surface d'attaque, pas seulement une ligne comptable — un endpoint sans frein que quelqu'un martèle est à la fois un trou de sécurité et une facture surprise, donc les plafonds (max_tokens, rate-limit, budget alerts à 80 %) sont de la sécurité, pas de la comptabilité. La facture ne se surveille pas en fin de mois ; on met le frein avant.
ERREURS À ÉVITER
- ×Croire que logguer = observer. Accumuler des logs que personne ne lit n'est pas de l'observabilité ; l'observabilité c'est avoir une poignée de signaux qui déclenchent des décisions. Une métrique qui n'actionne rien est du bruit : efface-la.
- ×Ignorer les erreurs client parce que 'le serveur ne reporte rien'. Dans un SSG le serveur ne PEUT PAS reporter le TypeError qui casse l'écran de l'utilisateur. Sans capture dans le navigateur, tu opères à l'aveugle sur la moitié de l'app.
- ×Ouvrir une table de logs au SELECT pour anon. Les logs s'insèrent depuis le client mais ne se lisent JAMAIS depuis lui (tu fuiterais les données d'autres utilisateurs). RLS d'insert seulement pour anon, lecture admin seulement — et passe get_advisors.
- ×Lancer une boucle d'agent IA ou une Edge Function sans max_tokens ni plafond d'itérations. C'est la cause n°1 de facture surprise : le coût escalade invisible pendant que 'ce que fait' l'app ne change pas.
- ×Surveiller la facture en fin de mois au lieu de mettre des budget alerts à 50/80/100 % avant. Quand tu vois la charge, tu l'as déjà payée ; l'alerte existe pour freiner, pas pour regretter.
- ×Fatigue d'alertes : configurer des avis pour tout jusqu'à ce que l'équipe les fasse taire. N'alerte que sur l'actionnable et l'urgent ; chaque alerte doit répondre à 'que fais-je quand elle se déclenche' ou elle ne devrait pas exister.
- ×Toucher les tables d'autres projets dans la DB partagée (iron_*, world_*, focus_*) en montant le logging. La table d'erreurs est de MOONKEY ; crée-la avec son propre préfixe et n'écrase pas XHUB IRON.
Constellation suivante
Operator Core
Opérer l'IA