Stack
Conventions
- Modules : ES modules uniquement (
"type": "module") - Formatage : tabs, double quotes — via Biome
- Imports packages : protocole
workspace:* - API JSX :
hono/jsxcomme jsxImportSource
Commandes qualité
Structure d’un package
API (Hono.js)
Architecture en couches :Base de données (Drizzle + Neon)
Le schéma TypeScript (packages/database/src/db/schema/) est la seule source de vérité. Une base
n’évolue que par les migrations commitées dans packages/database/src/drizzle/ : db:generate les
écrit depuis le schéma, db:migrate applique ce que déclare meta/_journal.json, rien d’autre.
DATABASE_URL et rien d’autre, db:generate et db:check compris
(la config l’exige même si la génération ne se connecte pas). drizzle-kit passe par le driver
pg (TCP) : il marche aussi bien contre un Postgres local que contre Neon.
Écrire une migration
- Modifier le schéma dans
packages/database/src/db/schema/. cd packages/database && bun run db:generate --name <ce_qui_change>, puis relire le SQL produit.- Un rattrapage de données (backfill) se fait dans une migration vide créée par
bunx drizzle-kit generate --config ./src/drizzle.config.ts --custom --name <nom>, jamais à la main hors journal : un SQL absent du journal est sauté sans un mot sur une base neuve. - Commiter le SQL et
meta/ensemble.db:checket le test du journal le vérifient. - L’appliquer sur ta base de dev (branche Neon
dev, celle de ton.env) :bun run db:migrate.
quality de ci.yml rejoue toute la chaîne sur un Postgres vide puis lance
db:check. Au merge sur main, le job migrate de build-images.yml migre la prod (secret
DATABASE_URL_MIGRATE) avant le déploiement, qui n’a pas lieu si la migration échoue.
Un changement qui casse le code déployé (colonne renommée, NOT NULL ajouté) se fait en deux temps :
d’abord ajouter, déployer, migrer les données ; ensuite retirer l’ancien dans une migration suivante.
Drizzle n’a pas de migration descendante : on corrige par une nouvelle migration, ou par une
restauration Neon à un instant donné.
Historique
Avant octobre 2026, la prod a évolué pardb:push et par des SQL écrits à la main : le dossier
comptait 37 fichiers mais le journal n’en déclarait que 20, et le premier (0001) ne passait pas
sur une base vide. Ces fichiers sont gardés dans src/drizzle/_archive/, que drizzle-kit ne lit
pas. Ils sont remplacés par une migration unique, 0000_baseline, générée depuis le schéma TS.
Runbook : basculer la prod sur la baseline
Exécuté le 2026-10-08 sur la branche Neon
main, répété d’abord sur une copie. Point de retour :
la branche backup/pre-baseline. Le runbook reste pour toute base créée avant cette date.db:migrate ne tourne en release. Sans ce marquage, db:migrate
tenterait de recréer toutes les tables : il échoue alors sur la première (already exists) et
annule tout, la migration tournant dans une transaction — sans dégât, mais sans déploiement.
1
a. Lire la prod, en lecture seule
Créer dans Neon un rôle en lecture seule (ou prendre l’URL de la branche de répétition) dans
PROD_URL_RO. drizzle-kit pull ne fait qu’introspecter :--out doit rester relatif (drizzle-kit préfixe ./ aux chemins absolus) ; prod-pull/ est
ignoré par git. Lire prod-pull/schema.ts pour repérer ce que l’étape suivante ne compare pas :
autre schéma que public, extensions, vues, types.2
b. Comparer la prod à la baseline
L’empreinte Ne pas comparer via Les noms de contraintes que le rejeu des 37 SQL archivés laissait prévoir (Vérifier :
src/scripts/schema-fingerprint.sql liste, triées, chaque colonne (type, nullabilité,
défaut), contrainte et index du schéma public. On la tire de la prod et d’un Postgres local
vide migré sur la baseline, puis on compare :drizzle-kit generate sur le snapshot tiré : l’introspection décrit index
et CHECK autrement que le schéma TS, et le diff les supprime et recrée tous.On ne bascule qu’avec un diff vide. Chaque écart se corrige avant :- ce que la prod a en trop ou de trave Écarts constatés en prod le 2026-10-08, corrigés en une transaction :
- deux clés primaires au nom des anciennes tables (
gifts_pkey,invitation_pkey) ; - les 7 index que supprimait l’archive
0036, jamais appliquée en prod ; - les 3
CHECKd’encaissement de l’archive0010, retirés par undb:push. Reposés enNOT VALIDpuisVALIDATE, ce qui ne bloque pas les écritures.
entitlements_*_key,
org_invitation_*) n’existaient pas : la prod a évolué par db:push, pas par ces fichiers.
Une autre base se compare toujours avant d’appliquer quoi que ce soit.TABLE “orders” VALIDATE CONSTRAINT “orders_amount_positive”;
ALTER TABLE “orders” VALIDATE CONSTRAINT “orders_status_valid”;
ALTER TABLE “payments” VALIDATE CONSTRAINT “payments_kind_valid”;DATABASE_URL="$PROD_URL" bunx turbo run db:migrate réussit sans rien appliquer
(une seule ligne de plus dans la table, l’empreinte de l’étape b inchangée).3
d. Revenir en arrière
- Avant le marquage, seul le correctif de l’étape b a touché la prod : inverser les
RENAME CONSTRAINT(noms permutés), recréer les index supprimés, retirer lesCHECKparDROP CONSTRAINT. Ou restaurer depuis la branche Neon de sauvegarde créée juste avant. - Après le marquage :
DELETE FROM drizzle.__drizzle_migrations WHERE created_at = 1791444919068;rend la table à son état d’avant. Aucune donnée n’a bougé. - Une migration ultérieure qui casse la prod : nouvelle migration corrective, ou restauration Neon
à un instant donné (branche créée depuis l’heure d’avant le
migrate).
