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)
| Chantier | Etat |
|---|---|
Ignored Build Step, previews d'agents opt-in, db push reserve a la production | Livre et merge (PR 1685). Verification en conditions reelles Vercel : a faire au premier build (ligne [ignore-build] dans les Build Logs) |
| Branches Neon | Cause 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 Vercel | Fait par le proprietaire |
| Ledger fournisseurs | Livre. Seuls Vercel, Neon et Stripe sont classes factures ; le reste est NON PROUVE tant qu'une facture n'est pas collee |
| Crons | Aucun 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 GitHub | Bloquee par la facturation Actions (jobs en 4 secondes, sans logs). Les PR de cette mission ont ete validees en local |
| Mesure de l'effet | Pas 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 sept | 0 | 43 (14 en ERROR) |
| 13 sept | 0 | 44 (4 en ERROR) |
| 21 sept | 3 | n/c |
| 22 sept | 44 (8 ERROR, 2 CANCELED) | n/c |
| 23 sept | 56 | n/c |
| 25 sept | 62 | n/c |
| 26 sept | 74 | n/c |
| 27 sept | 23 (3 ERROR) | n/c |
| 28 sept au 3 oct | 5, 1, 4, 19, 2 | n/c |
Ce que ca prouve :
- Chaque merge sur
mainproduit 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/*etclaude/*produisent un Preview par push : 87 previews sur 43 branches dans l'echantillon, dont plusieursBLOCKED,ERRORouCANCELED. 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_deploymentni l'API de facturation (list_billing_chargesrepond 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
CANCELEDceux qu'il annule, soit 2 dans l'echantillon. Les autres ont tourne jusqu'au bout.
Correction
- Ignored Build Step :
vercel.json->ignoreCommand=node scripts/vercel-ignore-build.mjs. Il compare le commit avecVERCEL_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. - Pas de liste arbitraire :
scripts/lib/build-policy.mjsliste les chemins inertes, etsrc/test/build-policy.test.tsle prouve sur le code : aucun fichier desrc/hors tests ne lit ces racines avecprocess.cwd(),next.config.mjsne 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 sauffacts.md, et les configs d'outils (README, eslint, vitest, playwright, knip, prettier, gitleaks). Volontairement non inertes :src/**(dontsrc/services/fleet/manifest.generated.json, lu a l'execution),content/,messages/,public/,prisma/,scripts/,package.json, lockfile,vercel.json,tsconfig.json. - 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.mainn'est jamais concerne par cette regle. - 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)
| Etape | Cout / necessite | Decision |
|---|---|---|
| Regeneration du schema guard | hors 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 canceledAt | garde |
prisma db push | deja 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 production | restreint a la production : shouldRunDbPush refuse tout VERCEL_ENV autre que production |
next build, heap 8 Go | le 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) :
| Route | Appels / 7 j | Source | Necessaire ? |
|---|---|---|---|
/intelligence/stores/[domain] | 4 557 | pages publiques SEO, robots | oui, mais voir cause |
/intelligence/directory/[kind]/[slug] | 2 158 | idem | idem |
/api/jobs/[type] | 1 285 | QStash | oui, borne par 3 retries puis DLQ |
/api/preview/browserbase/active/navigate et /[id]/logs | 488 + 488 | cockpit, polling | oui tant que le cockpit est ouvert |
/api/activity, /api/activity/stream, /api/events/stream | 340, 287, 231 | polling / SSE client | a surveiller |
| crons (reaper, ad-runs, gdpr, launch, harvest) | 288, 144, 96, 96, 96 | vercel.json | voir section 4 |
source middleware | 10 094 | src/proxy.ts | voir 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.
| Cron | Cadence | Appels/mois | Justification trouvee dans le code | Decision |
|---|---|---|---|---|
browser-session-reaper | */5 | 8 640 | Le 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 payantes | inchange |
intelligence/ad-runs-collect | */10 | 4 320 | collecte independante du tier de la boutique | inchange, a mesurer |
discovery-harvest-tick | 4/h | 2 880 | budget de temps (timeBudget), file de claims | inchange, a mesurer |
intelligence/liveness-sweep | */15 | 2 880 | lot dimensionne par capacite | inchange, a mesurer |
launch-tick | */15 | 2 880 | debit limite par le temps, pas par un plafond | inchange, a mesurer |
bulletin-dispatch | */15 | 2 880 | envoi planifie, 30 lignes | inchange, a mesurer |
gdpr-erasure | */15 | 2 880 | obligation legale, liste protegee de pause-policy | inchange, 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
neondu depot tombe, connecteur Neon sans projet visible, integration Vercel en 403, reseau de la session bloquantconsole.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 hebdomadaireneon-branch-janitor.yml. Rapport seul par defaut : liste chaque branche (created_at, derniere activite, branch-hours estimees, raison). Il ne supprime que sur--applyou la variableNEON_JANITOR_APPLY=true, et jamais la branche par defaut, une branche protegee, un parent, ni horspreview/. SansNEON_API_KEYil 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, soitmain(defaut, 298,7 Mo) et 347 branchespreview/claude/..., toutes creees par Vercel, parentmain,Idle, derniere activite entre 21 jours et 2 mois, stockage propre vide (copies copy-on-write demain). - 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 branchespreview/*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.ymlreste le filet hebdomadaire une fois GitHub Actions debloque et les secretsNEON_API_KEY/NEON_PROJECT_IDposes ; sans eux il ne fait rien.
6. Fournisseurs
Voir provider-cost-ledger.md.
7. Actions manuelles restantes (proprietaire)
Vercel > Settings > Billing > Spend Management : alerte + plafond durFait le 3 octobre (confirme par le proprietaire, valeurs non consignees ici).- 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. Vercel > Integrations > Neon : desactiver « branche par Preview » et nettoyer les branches existantesFait 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 danssrc/, puis regarder Neon > Branches). Optionnel : secretsNEON_API_KEY(secret) etNEON_PROJECT_ID(variable) pour le workflow hebdomadaire.- AI Gateway : budget par projet / cle API (tableau de bord).
- Plafonds chez Hume, Upstash Vector, Resend, ClickHouse (aucun plafond dans le code).
- Coller dans le ledger les montants reels par fournisseur (colonne FACTURE).
- Lancer la requete
CronExecutionci-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-policyechoue, c'est son role. Une lecture par un chemin construit dynamiquement (join(root, name)) echapperait a son regex : revue humaine. VERCEL_GIT_PREVIOUS_SHAabsent ou clone sans historique : le script construit (echec ouvert).- Un Preview d'agent voulu doit porter
[deploy preview].