ADR-0009 — Db push + guard genere plutot que migrations Prisma
Ce depot n'a pas de prisma/migrations/. Le repertoire n'existe pas, il n'a jamais existe, et prisma/schema.prisma:10 le dit : « pas de migrations, schema-first ».
Statut
Accepté · 2026-09-05
Piliers : data-platform, platform-ops
Contexte
Ce depot n'a pas de prisma/migrations/. Le repertoire n'existe pas, il
n'a jamais existe, et prisma/schema.prisma:10 le dit : « pas de
migrations, schema-first ».
Ce choix n'etait ecrit nulle part comme une decision. Consequence directe
(data-platform/0201) : AGENTS.md:126 enseignait
Run
pnpm db:push(dev) orpnpm prisma migrate dev --name <slug>(CI-tracked).
Or prisma migrate dev sans historique de migrations lit le schema vivant
comme un drift et propose de reinitialiser la base. C'est le fichier
sur lequel chaque agent de la flotte demarre, et il pointait un chemin
destructif vers la base Neon partagee — pendant que CLAUDE.md et
.claude/agents/data-platform-engineer.md interdisaient exactement ca.
Une convention non ecrite finit toujours par etre contredite par ecrit.
Options
(a) Adopter les migrations Prisma
Creer un prisma/migrations/ baseline depuis le schema courant, puis
migrate deploy au deploiement.
Ce que ca rend : un historique versionne, un chemin de rollback, la
capacite d'exprimer un ALTER et un backfill dans le meme fichier.
Ce que ca coute ici : le deploiement de ce projet n'a pas d'acces base au
moment du build. L'integration Vercel + Neon n'expose DATABASE_URL qu'au
runtime des fonctions, par conception. migrate deploy doit donc tourner
ailleurs — un job separe avec ses propres secrets, sa propre fenetre
d'echec, et un ordre a garantir vis-a-vis du deploiement. C'est une piece
d'infrastructure a operer, pas une commande a ajouter.
Plus le cout du baseline : 152 tables, 51 enums, 477 index et 135 cles
etrangeres a figer dans une migration initiale qui doit correspondre au
BIT PRES a ce qui est en production, sans quoi la premiere migrate deploy
diverge.
(b) db push + un guard genere, applique au cold start
Ce que fait le depot aujourd'hui : scripts/generate-schema-guard.mjs
derive du schema, sans connexion base, un catalogue complet et un DDL
idempotent ; src/instrumentation-node.ts diffe ce catalogue contre
information_schema au premier demarrage a froid et applique les seuls
steps manquants.
Décision
(b), et c'est desormais ecrit.
Trois raisons, dans l'ordre :
- Ca marche sans base au build. Le generateur tourne sur le seul
schema.prisma(prisma migrate diff --from-empty). Aucune cle, aucun reseau. C'est ce qui permet au guard d'etre regenere a CHAQUE build Vercel et verifie par le hook pre-push. - Ca ne peut rien detruire. Le generateur n'emet que de l'additif et
de l'idempotent :
CREATE TABLE IF NOT EXISTS,ADD COLUMN IF NOT EXISTS,ADD VALUE IF NOT EXISTS. UnDROPou unRENAMEne peut pas sortir de cette machine — il faut une action operateur deliberee. Avec des migrations, la meme garantie demande de la discipline a la relecture. - L'humain n'est plus dans la boucle. C'est le point qui a motive tout
le systeme : l'incident
Subscription.canceledAtdu 10 juin 2026 est arrive parce qu'un pas manuel avait ete oublie. Un catalogue derive ne s'oublie pas.
Conséquences
Ce que ce choix coute, et il faut le dire : le guard est additif. Il sait CREER, il ne sait pas ALTERER. Trois classes lui echappent, et chacune a sa propre porte :
| Classe | Ou elle vit | Ce qui la garde |
|---|---|---|
ALTER COLUMN (type, nullabilite) sur une colonne existante | EXTRA_STEPS dans pending-migrations.ts, avec une cible de detection | rien d'automatique — l'oubli est silencieux |
Index redondant ([a] quand [a, b] existe) | invisible au guard | pnpm db:indexes, liste d'acceptation qui ne peut que retrecir |
Colonne requise sans DEFAULT sur une table peuplee | Postgres la refuse (23502) | pnpm db:guard REFUSE de l'ecrire (data-platform/0200) |
La troisieme etait la plus dangereuse parce qu'elle etait invisible : le
generateur recopiait la definition Prisma telle quelle, donc un @updatedAt
ajoute a un modele existant produisait un step que le healer ne pourrait
jamais appliquer. Build vert, db:guard:check vert, puis P2022 en
production toutes les trente secondes. Exactement la forme de l'incident que
ce generateur existe pour rendre impossible.
Depuis data-platform/0200, le generateur compare la liste des colonnes
NOT NULL sans defaut a celle du catalogue precedemment commite et
refuse toute nouvelle entree, en nommant les trois sorties. La comparaison
doit se faire la : au moment du --check, le fichier commite contient deja
la colonne, donc les deux listes sont la meme liste.
Ce qui reste vrai des migrations : le jour ou un ALTER massif, un
backfill ordonne ou un rollback devient un besoin recurrent plutot qu'un
EXTRA_STEPS occasionnel, (a) redevient le bon choix. Le declencheur a
surveiller est le nombre d'entrees dans EXTRA_STEPS : quelques-unes sont
une exception saine, une douzaine veut dire que le systeme lutte contre son
propre modele.
Alternatives écartées
prisma db pushau build.scripts/vercel-build.mjsle tente et le saute : sur ce projet l'URL de base n'est pas dans l'environnement de BUILD. C'est journalise explicitement, et lire ce step comme « le deploy pousse le schema » est l'erreur que CLAUDE.md § Schema sync existe pour empecher.db push --accept-data-lossau deploy. Un deploiement ne doit jamais pouvoir supprimer des donnees. Un delta destructif est une action operateur, viapnpm db:deploy.