Runbooks & opérationsFinOps : durcir les couts de boostecom.app (3 octobre 2026)

FinOps : durcir les couts de boostecom.app (3 octobre 2026)

Perimetre prouve = ce que Vercel et le depot disent. Tout le reste est marque NON PROUVE avec la requete ou l'action qui le prouverait.

Perimetre prouve = ce que Vercel et le depot disent. Tout le reste est marque NON PROUVE avec la requete ou l'action qui le prouverait.

Etat au 3 octobre 2026 (a lire en premier)

ChantierEtat
Ignored Build Step, previews d'agents opt-in, db push reserve a la productionLivre et merge (PR 1685). Verification en conditions reelles Vercel : a faire au premier build (ligne [ignore-build] dans les Build Logs)
Branches NeonCause prouvee (integration Vercel + Neon, case Preview) et nettoyage fait par le proprietaire : case Preview decochee, branches preview/* supprimees, main conservee, cle API temporaire revoquee
Case Neon « Production »Laissee cochee, a decocher apres verification (voir section 7)
Spend Management VercelFait par le proprietaire
Ledger fournisseursLivre. Seuls Vercel, Neon et Stripe sont classes factures ; le reste est NON PROUVE tant qu'une facture n'est pas collee
CronsAucun modifie : pas de mesure du travail moyen sans acces a CronExecution
Invocations (~735k)Cause structurelle identifiee (nonce CSP, pas d'ISR), non corrigee : chantier a ouvrir
CI GitHubBloquee par la facturation Actions (jobs en 4 secondes, sans logs). Les PR de cette mission ont ete validees en local
Mesure de l'effetPas encore faite : comparer les deployments Preview et la facture Neon sur 7 jours puis sur le prochain cycle

1. Build CPU : ce qui est prouve

Source : list_deployments du projet boostecom.app (Vercel MCP), pages until successives. Fenetre couverte : 12 septembre au 3 octobre 2026, 387 deployments listes (les 14 au 20 septembre manquent a la pagination : le decompte est un plancher).

Jour (UTC)Production (main)Preview
12 sept043 (14 en ERROR)
13 sept044 (4 en ERROR)
21 sept3n/c
22 sept44 (8 ERROR, 2 CANCELED)n/c
23 sept56n/c
25 sept62n/c
26 sept74n/c
27 sept23 (3 ERROR)n/c
28 sept au 3 oct5, 1, 4, 19, 2n/c

Ce que ca prouve :

  • Chaque merge sur main produit un deployment de production. Les 25 et 26 septembre (jours a ~266 et ~339 commits d'apres le signal Git du proprietaire) donnent 136 builds de production en deux jours.
  • Les branches fleet/* et claude/* produisent un Preview par push : 87 previews sur 43 branches dans l'echantillon, dont plusieurs BLOCKED, ERROR ou CANCELED. Un meme commit de simple lien de backlog (chore(growth-web): link backlog 2681 to PR 1221) a donc declenche un build complet.
  • Le createur de tous ces deployments est le meme compte (boostecom-ecosystem) : agents et humains passent par la meme integration Git.

Ce qui reste NON PROUVE :

  • Les minutes de Build CPU par deployment : ni get_deployment ni l'API de facturation (list_billing_charges repond 404 « Plan not found ») ne les exposent avec ce jeton. Le rapprochement jour par jour avec les 47 508 minutes est donc un ordre de grandeur (nombre de builds), pas une mesure.
  • Combien de ces builds etaient immediatement remplaces : Vercel marque CANCELED ceux qu'il annule, soit 2 dans l'echantillon. Les autres ont tourne jusqu'au bout.

Correction

  1. Ignored Build Step : vercel.json -> ignoreCommand = node scripts/vercel-ignore-build.mjs. Il compare le commit avec VERCEL_GIT_PREVIOUS_SHA (dernier commit reellement construit) et saute le build quand tous les fichiers modifies sont hors artefact. Il echoue toujours « ouvert » : diff impossible = on construit.
  2. Pas de liste arbitraire : scripts/lib/build-policy.mjs liste les chemins inertes, et src/test/build-policy.test.ts le prouve sur le code : aucun fichier de src/ hors tests ne lit ces racines avec process.cwd(), next.config.mjs ne les trace pas, et le fichier trace dans /api/chat ($HOME/.claude/skills/boostecom-content/references/facts.md) reste constructible. Racines inertes : docs/, backlog/, e2e/, evals/, creative/, .github/, .cursor/, .claude/agents|fleet, les skills sauf facts.md, et les configs d'outils (README, eslint, vitest, playwright, knip, prettier, gitleaks). Volontairement non inertes : src/** (dont src/services/fleet/manifest.generated.json, lu a l'execution), content/, messages/, public/, prisma/, scripts/, package.json, lockfile, vercel.json, tsconfig.json.
  3. Previews d'agents opt-in : sur fleet/*, claude/*, cursor/*, codex/*, copilot/*, un Preview ne se construit que si le message du commit contient [deploy preview]. Un humain sur une autre branche garde son Preview. main n'est jamais concerne par cette regle.
  4. Regle de push (docs/team/protocol.md §4) : commit local != push distant != deployment. Un push par unite logique, pas par micro-changement.

Effet attendu (a verifier apres merge, pas une mesure) : les previews d'agents tombent a zero par defaut ; un merge qui ne touche que docs/ ou backlog/ ne construit plus. Les merges qui touchent src/ construisent toujours : c'est voulu.

2. Le build lui-meme (scripts/vercel-build.mjs)

EtapeCout / necessiteDecision
Regeneration du schema guardhors ligne (prisma migrate diff --from-empty), pas de DB. Le build echoue si le guard est stale : c'est le filet qui evite l'incident canceledAtgarde
prisma db pushdeja saute en pratique : l'integration Vercel + Neon n'expose pas DATABASE_URL au build (docs/architecture/database.md). Si la variable etait posee, un Preview d'une branche non mergee remodelerait la base unique, qui est la productionrestreint a la production : shouldRunDbPush refuse tout VERCEL_ENV autre que production
next build, heap 8 Gole gros du cout, incompressible sans changer l'application. Le typecheck est deja desactive (REQUIRE_BUILD_TYPECHECK)garde

Sortir db push du build au profit d'un workflow : non fait, sans objet. L'etape est inerte en production et le cold-start (ensureSchemaGuard) est le mecanisme reel. La migrer ajouterait un workflow qui touche la production pour un gain nul.

3. Les ~735k invocations

Les 71 crons font ~38 200 appels/mois (calcule depuis vercel.json). Il manque ~700k.

Echantillon des journaux d'execution (7 jours, production, Vercel runtime logs, groupes par route : c'est un echantillon de journaux, pas le compteur de facturation, l'API Observability repond 404 pour ce jeton) :

RouteAppels / 7 jSourceNecessaire ?
/intelligence/stores/[domain]4 557pages publiques SEO, robotsoui, mais voir cause
/intelligence/directory/[kind]/[slug]2 158idemidem
/api/jobs/[type]1 285QStashoui, borne par 3 retries puis DLQ
/api/preview/browserbase/active/navigate et /[id]/logs488 + 488cockpit, pollingoui tant que le cockpit est ouvert
/api/activity, /api/activity/stream, /api/events/stream340, 287, 231polling / SSE clienta surveiller
crons (reaper, ad-runs, gdpr, launch, harvest)288, 144, 96, 96, 96vercel.jsonvoir section 4
source middleware10 094src/proxy.tsvoir cause

Cause structurelle prouvee dans le code : src/app/(marketing)/intelligence/stores/[domain]/page.tsx declare revalidate = 21_600 mais le commentaire du fichier etablit qu'il est inerte : le layout racine lit la requete (cookies(), headers() pour le nonce CSP), ce qui desactive l'ISR pour toute l'application (src/test/isr-is-disabled-by-the-nonce.test.ts, items app-shell/0189, 0361, 0365). Chaque passage de robot sur une page publique coute donc une invocation de fonction et une du middleware (src/proxy.ts tourne sur tout sauf /api). C'est le candidat n°1 pour la masse des invocations. Le corriger (nonce par page ou CSP sans nonce sur le marketing) est un chantier de securite et d'architecture, hors du perimetre de cette PR : il est trace dans le backlog.

Autres sources inspectees : retries QStash (3 puis DLQ, dlq-retry borne par tick), webhooks, appels internes. Aucun retry sans borne trouve dans services/jobs.

4. Crons de moins d'une heure

Appels/mois calcules depuis vercel.json. « Travail moyen » exige les lignes CronExecution de la base de production, que cette session ne peut pas lire : la requete est fournie en fin de section.

CronCadenceAppels/moisJustification trouvee dans le codeDecision
browser-session-reaper*/58 640Le commentaire du fichier documente que la cadence EST le cout : session reapable 3 min apres le dernier poll, liberee au tick suivant ; */30 avait fait bruler 30 min Browserbase payantesinchange
intelligence/ad-runs-collect*/104 320collecte independante du tier de la boutiqueinchange, a mesurer
discovery-harvest-tick4/h2 880budget de temps (timeBudget), file de claimsinchange, a mesurer
intelligence/liveness-sweep*/152 880lot dimensionne par capaciteinchange, a mesurer
launch-tick*/152 880debit limite par le temps, pas par un plafondinchange, a mesurer
bulletin-dispatch*/152 880envoi planifie, 30 lignesinchange, a mesurer
gdpr-erasure*/152 880obligation legale, liste protegee de pause-policyinchange, ne jamais pauser

Kill switch : deja en place. withCronAuth lit une pause runtime depuis /admin/platform/crons (src/services/cron/pause.ts), fail-open, avec une liste protegee (RGPD, facturation, paiements). Aucune frequence n'est reduite sans mesure : ce serait arbitraire.

Mesure a lancer par le proprietaire (lecture seule) :

SELECT "cronName", count(*) AS runs,
       round(avg("durationMs")) AS avg_ms,
       count(*) FILTER (WHERE "durationMs" < 500) AS fast_noop
FROM "CronExecution" WHERE "startedAt" > now() - interval '7 days'
GROUP BY 1 ORDER BY runs DESC;

Un cron dont fast_noop approche runs est un candidat a l'evenementiel (QStash) : a traiter par item, avec la mesure en main.

5. Neon

  • Le depot ne cree aucune branche Neon : aucun workflow, script ni code ne l'appelle. Les branches viennent donc de l'integration Vercel + Neon (reglage « creer une branche par Preview deployment ») ou d'un autre acteur.
  • Les branches ont d'abord ete impossibles a lister depuis cette session (MCP neon du depot tombe, connecteur Neon sans projet visible, integration Vercel en 403, reseau de la session bloquant console.neon.tech). La console Neon, vue par le proprietaire, a ensuite apporte la preuve ci-dessous : l'attribution a ce projet est etablie.
  • Ce qui reste non verifie : la part exacte des 389,54 USD / 186 979 branch-hours due a ces branches. Les ~540 h par branche (voir plus bas) sont coherentes avec la facture, mais seule la prochaine facture Neon le confirme.
  • Livre : scripts/neon-branch-janitor.mjs + workflow hebdomadaire neon-branch-janitor.yml. Rapport seul par defaut : liste chaque branche (created_at, derniere activite, branch-hours estimees, raison). Il ne supprime que sur --apply ou la variable NEON_JANITOR_APPLY=true, et jamais la branche par defaut, une branche protegee, un parent, ni hors preview/. Sans NEON_API_KEY il ne fait rien.
  • Avec les previews d'agents coupes par defaut, le flux de nouvelles branches preview s'arrete a la source.

Preuve apportee par le proprietaire (captures Neon et Vercel, 3 octobre)

  • Projet Neon boostecom-app, lie a ce projet Vercel : 348 branches, soit main (defaut, 298,7 Mo) et 347 branches preview/claude/..., toutes creees par Vercel, parent main, Idle, derniere activite entre 21 jours et 2 mois, stockage propre vide (copies copy-on-write de main).
  • L'integration Vercel + Neon avait « Create Database Branch For Deployment » coche sur Preview et Production. Preview a ete decoche et sauvegarde par le proprietaire le 3 octobre. Production est laisse coche en attendant de prouver qu'il ne cree pas de branche (aucune branche de production vue).
  • Ordre de grandeur, pas une facture : 186 979 branch-hours / 347 branches = ~540 h, soit ~22 jours par branche, coherent avec l'age observe.
  • Le proprietaire est le seul utilisateur de la base pour l'instant : aucune donnee tierce sur ces branches.

Nettoyage : lancer scripts/neon-branch-janitor.mjs en rapport depuis un poste (NEON_API_KEY, NEON_PROJECT_ID), relire, puis --apply. Ne marche pas via le workflow tant que GitHub Actions est bloque par la facturation.

Resultat du nettoyage (3 octobre 2026)

  • Le proprietaire a execute le janitor depuis son poste (rapport, relecture, puis --apply) et confirme que les branches preview/* sont supprimees. main (defaut) est conservee.
  • La cle API temporaire a ete revoquee apres usage.
  • Etat apres nettoyage : propre, confirme par le proprietaire (avant : 348 branches, dont 347 preview/*). Le nombre exact restant n'a pas ete releve ; il n'est pas necessaire pour la suite, la verification se fait en regardant que la page Neon > Branches ne se repeuple pas.
  • Effet sur la facture : NON MESURE. A comparer sur la prochaine facture Neon (avant : 389,54 USD de branches supplementaires, 186 979 branch-hours).
  • Le workflow neon-branch-janitor.yml reste le filet hebdomadaire une fois GitHub Actions debloque et les secrets NEON_API_KEY / NEON_PROJECT_ID poses ; sans eux il ne fait rien.

6. Fournisseurs

Voir provider-cost-ledger.md.

7. Actions manuelles restantes (proprietaire)

  1. Vercel > Settings > Billing > Spend Management : alerte + plafond dur Fait le 3 octobre (confirme par le proprietaire, valeurs non consignees ici).
  2. Vercel > Project > Settings > Git : verifier « Ignored Build Step » = commande du vercel.json (le fichier prime), et Build Queue : activer l'annulation des builds obsoletes si l'option est proposee.
  3. Vercel > Integrations > Neon : desactiver « branche par Preview » et nettoyer les branches existantes Fait le 3 octobre (Preview decoche, janitor execute). Reste : decocher Production seulement apres avoir verifie qu'un build de production ne cree pas de branche (merge un petit changement dans src/, puis regarder Neon > Branches). Optionnel : secrets NEON_API_KEY (secret) et NEON_PROJECT_ID (variable) pour le workflow hebdomadaire.
  4. AI Gateway : budget par projet / cle API (tableau de bord).
  5. Plafonds chez Hume, Upstash Vector, Resend, ClickHouse (aucun plafond dans le code).
  6. Coller dans le ledger les montants reels par fournisseur (colonne FACTURE).
  7. Lancer la requete CronExecution ci-dessus et rouvrir les crons concernes.

8. Risques

  • L'Ignored Build Step saute un build sans test : si un fichier lu a l'execution est ajoute plus tard dans une racine inerte, le test build-policy echoue, c'est son role. Une lecture par un chemin construit dynamiquement (join(root, name)) echapperait a son regex : revue humaine.
  • VERCEL_GIT_PREVIOUS_SHA absent ou clone sans historique : le script construit (echec ouvert).
  • Un Preview d'agent voulu doit porter [deploy preview].